Compare commits

...

2 Commits

Author SHA1 Message Date
jess c18ee2c226 lib 2026-05-30 16:39:53 -07:00
jess 6c3066fa1d decomposer/compiler 2026-05-30 15:58:00 -07:00
10 changed files with 507 additions and 47 deletions

View File

@ -49,6 +49,11 @@ pub fn decompose(source: &str) -> Result<Decomposed, String> {
/// decomposes with a custom hook for external extensions. /// decomposes with a custom hook for external extensions.
pub fn decompose_with(source: &str, hook: &dyn DecomposeHook) -> Result<Decomposed, String> { pub fn decompose_with(source: &str, hook: &dyn DecomposeHook) -> Result<Decomposed, String> {
let stmts = parse_program(source)?; 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 deps = Vec::new();
let mut out = String::new(); let mut out = String::new();
out.push_str(GENERATED_PRELUDE); out.push_str(GENERATED_PRELUDE);
@ -56,18 +61,28 @@ pub fn decompose_with(source: &str, hook: &dyn DecomposeHook) -> Result<Decompos
out.push_str(&extra); out.push_str(&extra);
out.push('\n'); out.push('\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 })
} }
/// dispatch-table entry: type, method, fn name, arity.
struct MethodReg {
ty: String,
method: String,
fn_name: String,
arity: usize,
}
fn emit_program(out: &mut String, stmts: &[Stmt], deps: &mut Vec<Dependency>, hook: &dyn DecomposeHook) -> Result<(), String> { fn emit_program(out: &mut String, stmts: &[Stmt], deps: &mut Vec<Dependency>, hook: &dyn DecomposeHook) -> Result<(), String> {
let mut uses = Vec::new(); let mut uses = Vec::new();
let mut fns = Vec::new(); let mut fns = Vec::new();
let mut impls = Vec::new();
let mut rest = Vec::new(); let mut rest = Vec::new();
for s in stmts { for s in stmts {
match s { match s {
Stmt::Use(..) => uses.push(s), Stmt::Use(..) => uses.push(s),
Stmt::FnDef { .. } => fns.push(s), Stmt::FnDef { .. } => fns.push(s),
Stmt::ImplBlock { .. } | Stmt::TraitDef { .. } => impls.push(s),
_ => rest.push(s), _ => rest.push(s),
} }
} }
@ -81,15 +96,77 @@ fn emit_program(out: &mut String, stmts: &[Stmt], deps: &mut Vec<Dependency>, ho
emit_stmt(out, s, 0, deps, hook)?; emit_stmt(out, s, 0, deps, hook)?;
out.push('\n'); out.push('\n');
} }
let mut regs: Vec<MethodReg> = Vec::new();
for s in &impls {
if let Stmt::ImplBlock { type_name, methods, .. } = s {
emit_impl(out, type_name, methods, &mut regs, deps, hook)?;
out.push('\n');
}
}
indent(out, 0, "pub fn run() -> V {"); indent(out, 0, "pub fn run() -> V {");
for s in &rest { for s in &rest {
emit_stmt(out, s, 1, deps, hook)?; emit_stmt(out, s, 1, deps, hook)?;
} }
indent(out, 1, "V::Void"); indent(out, 1, "V::Void");
indent(out, 0, "}"); indent(out, 0, "}");
out.push('\n');
emit_dispatch(out, &regs);
Ok(()) Ok(())
} }
/// emits each impl method as a dispatch-registered free function.
fn emit_impl(
out: &mut String,
type_name: &str,
methods: &[Stmt],
regs: &mut Vec<MethodReg>,
deps: &mut Vec<Dependency>,
hook: &dyn DecomposeHook,
) -> Result<(), String> {
for m in methods {
let Stmt::FnDef { name, params, body, .. } = m else { continue };
let fn_name = format!("{}__{}", ident(type_name), ident(name));
let param_list: String = params.iter()
.map(|(p, _)| format!("mut {}: V", ident(p)))
.collect::<Vec<_>>()
.join(", ");
indent(out, 0, &format!("pub fn {}({}) -> V {{", fn_name, param_list));
for s in body {
emit_stmt(out, s, 1, deps, hook)?;
}
indent(out, 1, "V::Void");
indent(out, 0, "}");
regs.push(MethodReg {
ty: type_name.to_string(),
method: name.clone(),
fn_name,
arity: params.len(),
});
}
Ok(())
}
/// emits the runtime dispatch table keyed on the struct __type tag.
fn emit_dispatch(out: &mut String, regs: &[MethodReg]) {
out.push_str("fn v_dispatch(ty: &str, method: &str, args: &[V]) -> Option<V> {\n");
out.push_str(" match (ty, method) {\n");
for r in regs {
let call_args: String = (0..r.arity)
.map(|i| format!("args.get({}).cloned().unwrap_or(V::Void)", i))
.collect::<Vec<_>>()
.join(", ");
out.push_str(&format!(
" ({:?}, {:?}) => Some({}({})),\n",
r.ty, r.method, r.fn_name, call_args
));
}
out.push_str(" _ => None,\n");
out.push_str(" }\n}\n");
}
fn emit_stmt(out: &mut String, stmt: &Stmt, depth: usize, deps: &mut Vec<Dependency>, hook: &dyn DecomposeHook) -> Result<(), String> { fn emit_stmt(out: &mut String, stmt: &Stmt, depth: usize, deps: &mut Vec<Dependency>, hook: &dyn DecomposeHook) -> Result<(), String> {
if let Some(custom) = hook.stmt(stmt) { if let Some(custom) = hook.stmt(stmt) {
indent(out, depth, &custom); indent(out, depth, &custom);
@ -105,9 +182,13 @@ fn emit_stmt(out: &mut String, stmt: &Stmt, depth: usize, deps: &mut Vec<Depende
indent(out, depth, &format!("{} = {};", ident(name), rhs)); indent(out, depth, &format!("{} = {};", ident(name), rhs));
} }
Stmt::PathAssign(lhs, expr) => { Stmt::PathAssign(lhs, expr) => {
let lhs_s = emit_expr(lhs, hook)?; let mut steps = Vec::new();
let root = collect_lvalue_path(lhs, &mut steps, hook)?;
let rhs = emit_expr(expr, hook)?; let rhs = emit_expr(expr, hook)?;
indent(out, depth, &format!("v_path_assign(&mut {}, &{});", lhs_s, rhs)); indent(out, depth, &format!(
"v_assign_path(&mut {}, &[{}], {});",
root, steps.join(", "), rhs
));
} }
Stmt::Return(expr) => { Stmt::Return(expr) => {
let rhs = emit_expr(expr, hook)?; let rhs = emit_expr(expr, hook)?;
@ -194,28 +275,31 @@ fn emit_stmt(out: &mut String, stmt: &Stmt, depth: usize, deps: &mut Vec<Depende
)); ));
indent(out, depth, "}"); indent(out, depth, "}");
} }
Stmt::TraitDef { name, methods } => { // hoisted to free functions in emit_program.
indent(out, depth, &format!("pub trait {} {{", ident(name))); Stmt::TraitDef { .. } | Stmt::ImplBlock { .. } => {}
for m in methods {
indent(out, depth + 1, &format!("fn {}(&self) -> V;", ident(m)));
}
indent(out, depth, "}");
}
Stmt::ImplBlock { type_name, trait_name, methods } => {
let header = match trait_name {
Some(t) => format!("impl {} for {} {{", ident(t), ident(type_name)),
None => format!("impl {} {{", ident(type_name)),
};
indent(out, depth, &header);
for m in methods {
emit_stmt(out, m, depth + 1, deps, hook)?;
}
indent(out, depth, "}");
}
} }
Ok(()) Ok(())
} }
/// walks a Field/Index lvalue chain into a root identifier plus emitted Step nodes.
fn collect_lvalue_path(expr: &Expr, steps: &mut Vec<String>, hook: &dyn DecomposeHook) -> Result<String, String> {
match expr {
Expr::Ident(name) => Ok(ident(name)),
Expr::Field(inner, field) => {
let root = collect_lvalue_path(inner, steps, hook)?;
steps.push(format!("Step::Field({:?}.into())", field));
Ok(root)
}
Expr::Index(inner, idx) => {
let root = collect_lvalue_path(inner, steps, hook)?;
let i = emit_expr(idx, hook)?;
steps.push(format!("Step::Index(v_num(&{}) as i64)", i));
Ok(root)
}
_ => Err("assignment target must root at a variable".into()),
}
}
fn emit_expr(expr: &Expr, hook: &dyn DecomposeHook) -> Result<String, String> { fn emit_expr(expr: &Expr, hook: &dyn DecomposeHook) -> Result<String, String> {
if let Some(custom) = hook.expr(expr) { if let Some(custom) = hook.expr(expr) {
return Ok(custom); return Ok(custom);
@ -344,6 +428,7 @@ fn indent(out: &mut String, depth: usize, line: &str) {
} }
fn ident(name: &str) -> String { fn ident(name: &str) -> String {
if name == "self" { return "self_".into(); }
if RESERVED.contains(&name) { format!("r#{}", name) } else { name.replace('-', "_") } if RESERVED.contains(&name) { format!("r#{}", name) } else { name.replace('-', "_") }
} }
@ -405,25 +490,41 @@ 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 } } 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(), _ => 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_neg(a: &V) -> V { V::Num(-v_num(a)) } fn v_struct_type(v: &V) -> Option<String> {
fn v_not(a: &V) -> V { V::Bool(!v_truthy(a)) } if let V::Struct(m) = v {
fn v_strip(a: &V) -> V { match a { V::Str(s) => V::Str(s.trim().into()), _ => a.clone() } } if let Some(V::Str(t)) = m.get("__type") { return Some(t.clone()); }
}
None
}
fn v_add(a: &V, b: &V) -> V { match (a, b) { (V::Str(s1), V::Str(s2)) => V::Str(format!("{}{}", s1, s2)), (V::Array(a1), V::Array(a2)) => V::Array([a1.as_slice(), a2.as_slice()].concat()), _ => V::Num(v_num(a) + v_num(b)) } } fn v_binop_overload(method: &str, a: &V, b: &V) -> Option<V> {
fn v_sub(a: &V, b: &V) -> V { V::Num(v_num(a) - v_num(b)) } if let Some(t) = v_struct_type(a) { return v_dispatch(&t, method, &[a.clone(), b.clone()]); }
fn v_mul(a: &V, b: &V) -> V { V::Num(v_num(a) * v_num(b)) } None
fn v_div(a: &V, b: &V) -> V { let d = v_num(b); V::Num(if d == 0.0 { f64::NAN } else { v_num(a) / d }) } }
fn v_rem(a: &V, b: &V) -> V { V::Num(v_num(a) % v_num(b)) } fn v_unop_overload(method: &str, a: &V) -> Option<V> {
fn v_pow(a: &V, b: &V) -> V { V::Num(v_num(a).powf(v_num(b))) } if let Some(t) = v_struct_type(a) { return v_dispatch(&t, method, &[a.clone()]); }
None
}
fn v_eq(a: &V, b: &V) -> V { V::Bool(a == b) } fn v_neg(a: &V) -> V { if let Some(r) = v_unop_overload("neg", a) { return r; } V::Num(-v_num(a)) }
fn v_neq(a: &V, b: &V) -> V { V::Bool(a != b) } fn v_not(a: &V) -> V { if let Some(r) = v_unop_overload("not", a) { return r; } V::Bool(!v_truthy(a)) }
fn v_lt(a: &V, b: &V) -> V { V::Bool(v_num(a) < v_num(b)) } fn v_strip(a: &V) -> V { if let Some(r) = v_unop_overload("strip", a) { return r; } match a { V::Str(s) => V::Str(s.trim().into()), V::Bool(b) => V::Num(if *b { 1.0 } else { 0.0 }), _ => a.clone() } }
fn v_gt(a: &V, b: &V) -> V { V::Bool(v_num(a) > v_num(b)) }
fn v_lte(a: &V, b: &V) -> V { V::Bool(v_num(a) <= v_num(b)) } fn v_add(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("add", a, b) { return r; } match (a, b) { (V::Str(s1), V::Str(s2)) => V::Str(format!("{}{}", s1, s2)), (V::Str(s), o) => V::Str(format!("{}{}", s, o)), (o, V::Str(s)) => V::Str(format!("{}{}", o, s)), (V::Array(a1), V::Array(a2)) => V::Array([a1.as_slice(), a2.as_slice()].concat()), (V::Array(a1), o) => { let mut v = a1.clone(); v.push(o.clone()); V::Array(v) }, (o, V::Array(a2)) => { let mut v = vec![o.clone()]; v.extend(a2.iter().cloned()); V::Array(v) }, _ => V::Num(v_num(a) + v_num(b)) } }
fn v_gte(a: &V, b: &V) -> V { V::Bool(v_num(a) >= v_num(b)) } fn v_sub(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("sub", a, b) { return r; } V::Num(v_num(a) - v_num(b)) }
fn v_mul(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("mul", a, b) { return r; } V::Num(v_num(a) * v_num(b)) }
fn v_div(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("div", a, b) { return r; } let d = v_num(b); V::Num(if d == 0.0 { f64::NAN } else { v_num(a) / d }) }
fn v_rem(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("rem", a, b) { return r; } V::Num(v_num(a) % v_num(b)) }
fn v_pow(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("pow", a, b) { return r; } V::Num(v_num(a).powf(v_num(b))) }
fn v_eq(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("eq", a, b) { return r; } V::Bool(a == b) }
fn v_neq(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("ne", a, b) { return r; } V::Bool(a != b) }
fn v_lt(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("lt", a, b) { return r; } V::Bool(v_num(a) < v_num(b)) }
fn v_gt(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("gt", a, b) { return r; } V::Bool(v_num(a) > v_num(b)) }
fn v_lte(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("le", a, b) { return r; } V::Bool(v_num(a) <= v_num(b)) }
fn v_gte(a: &V, b: &V) -> V { if let Some(r) = v_binop_overload("ge", a, b) { return r; } V::Bool(v_num(a) >= v_num(b)) }
fn v_and(a: &V, b: &V) -> V { V::Bool(v_truthy(a) && v_truthy(b)) } fn v_and(a: &V, b: &V) -> V { V::Bool(v_truthy(a) && v_truthy(b)) }
fn v_or(a: &V, b: &V) -> V { V::Bool(v_truthy(a) || v_truthy(b)) } fn v_or(a: &V, b: &V) -> V { V::Bool(v_truthy(a) || v_truthy(b)) }
@ -465,8 +566,31 @@ fn v_field(base: &V, name: &str) -> V {
} }
} }
fn v_field_set(base: &mut V, _val: &V) { enum Step { Field(String), Index(i64) }
let _ = base; // path assignment placeholder
fn v_assign_path(root: &mut V, steps: &[Step], val: V) {
let mut cur = root;
for (i, step) in steps.iter().enumerate() {
let last = i + 1 == steps.len();
match step {
Step::Field(k) => match cur {
V::Struct(m) => {
if last { m.insert(k.clone(), val); return; }
cur = m.entry(k.clone()).or_insert(V::Void);
}
_ => return,
},
Step::Index(idx) => match cur {
V::Array(a) => {
let n = if *idx < 0 { (a.len() as i64 + idx) as usize } else { *idx as usize };
if n >= a.len() { return; }
if last { a[n] = val; return; }
cur = &mut a[n];
}
_ => return,
},
}
}
} }
fn v_iter(val: &V) -> Vec<V> { fn v_iter(val: &V) -> Vec<V> {
@ -477,14 +601,20 @@ fn v_iter(val: &V) -> Vec<V> {
} }
} }
fn v_path_assign(target: &mut V, val: &V) { *target = val.clone(); }
fn v_cell_get(_table: &str, _col: u32, _row: u32) -> V { V::Void } fn v_cell_get(_table: &str, _col: u32, _row: u32) -> V { V::Void }
fn v_cell_range(_table: &str, _c1: u32, _r1: u32, _c2: u32, _r2: u32) -> V { V::Array(Vec::new()) } fn v_cell_range(_table: &str, _c1: u32, _r1: u32, _c2: u32, _r2: u32) -> V { V::Array(Vec::new()) }
fn v_cell_table(_table: &str) -> V { V::Array(Vec::new()) } fn v_cell_table(_table: &str) -> V { V::Array(Vec::new()) }
fn v_cell_set(_table: &str, _col: u32, _row: u32, _val: &V) {} fn v_cell_set(_table: &str, _col: u32, _row: u32, _val: &V) {}
fn v_solve_newton(_target_var: &str, _source_fn: &str) -> V { V::Void } fn v_solve_newton(_target_var: &str, _source_fn: &str) -> V { V::Void }
fn v_solve_call(_var: &str, _source_fn: &str) -> V { V::Void } fn v_solve_call(_var: &str, _source_fn: &str) -> V { V::Void }
fn v_method_call(_recv: &V, _method: &str, _args: &[V]) -> V { V::Void } fn v_method_call(recv: &V, method: &str, args: &[V]) -> V {
if let Some(t) = v_struct_type(recv) {
let mut full = vec![recv.clone()];
full.extend_from_slice(args);
if let Some(r) = v_dispatch(&t, method, &full) { return r; }
}
V::Void
}
// --- generated code below --- // --- generated code below ---
@ -616,4 +746,101 @@ mod tests {
assert!(r.code.contains("use sitter::scene::primitives;")); assert!(r.code.contains("use sitter::scene::primitives;"));
assert_eq!(r.deps[0].segments, vec!["sitter", "scene", "primitives"]); assert_eq!(r.deps[0].segments, vec!["sitter", "scene", "primitives"]);
} }
#[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}");
assert!(out.contains("pub fn Vec2__new("));
assert!(out.contains("pub fn Vec2__add(mut self_: V, mut other: V)"));
assert!(out.contains("(\"Vec2\", \"add\") => Some(Vec2__add("));
assert!(!out.contains("impl Vec2"));
}
/// rustc compile-and-run check of operator overloading and method dispatch.
#[test]
fn decomposed_binary_runs_with_correct_results() {
let src = "\
impl Vec2 {
fn new(x, y) {
return {__type: \"Vec2\", x: x, y: y}
}
fn add(self, other) {
return Vec2::new(self.x + other.x, self.y + other.y)
}
fn len2(self) {
return self.x * self.x + self.y * self.y
}
}
let a = Vec2::new(1, 2)
let b = Vec2::new(3, 4)
let c = a + b
let p = {x: 1, y: 2}
p.x = 10
let arr = [1, 2, 3]
arr[1] = 20
let node = {pose: {pos: {x: 0}}}
node.pose.pos.x = 5
";
let mut code = dec(src);
assert!(code.contains("v_assign_path(&mut p"));
assert!(code.contains("v_assign_path(&mut arr"));
assert!(code.contains("v_assign_path(&mut node"));
code.push_str(r#"
fn main() {
let a = Vec2__new(V::Num(1.0), V::Num(2.0));
let b = Vec2__new(V::Num(3.0), V::Num(4.0));
let c = v_add(&a, &b);
assert_eq!(v_field(&c, "x"), V::Num(4.0));
assert_eq!(v_field(&c, "y"), V::Num(6.0));
let l = v_method_call(&a, "len2", &[]);
assert_eq!(l, V::Num(5.0));
let mut p = V::Struct([("x".to_string(), V::Num(1.0))].into_iter().collect());
v_assign_path(&mut p, &[Step::Field("x".into())], V::Num(10.0));
assert_eq!(v_field(&p, "x"), V::Num(10.0));
let mut arr = V::Array(vec![V::Num(1.0), V::Num(2.0), V::Num(3.0)]);
v_assign_path(&mut arr, &[Step::Index(1)], V::Num(20.0));
assert_eq!(v_index(&arr, &V::Num(1.0)), V::Num(20.0));
let mut node = V::Struct([("pose".to_string(), V::Struct([("pos".to_string(), V::Struct([("x".to_string(), V::Num(0.0))].into_iter().collect()))].into_iter().collect()))].into_iter().collect());
v_assign_path(&mut node, &[Step::Field("pose".into()), Step::Field("pos".into()), Step::Field("x".into())], V::Num(5.0));
assert_eq!(v_field(&v_field(&v_field(&node, "pose"), "pos"), "x"), V::Num(5.0));
let _ = run();
println!("OK");
}
"#);
let dir = std::env::temp_dir().join(format!("acord-dec-compile-{}", 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{}\n--- source ---\n{}",
String::from_utf8_lossy(&compile.stderr),
code
);
let run = std::process::Command::new(&bin_path).output().unwrap();
assert!(run.status.success(), "binary panicked:\n{}", String::from_utf8_lossy(&run.stderr));
assert_eq!(String::from_utf8_lossy(&run.stdout).trim(), "OK");
std::fs::remove_dir_all(&dir).ok();
}
} }

View File

@ -1,4 +1,4 @@
#[derive(Debug, Clone)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Op { pub enum Op {
Add, Sub, Mul, Div, Mod, Pow, Add, Sub, Mul, Div, Mod, Pow,
Eq, Neq, Lt, Gt, Lte, Gte, Eq, Neq, Lt, Gt, Lte, Gte,
@ -6,6 +6,30 @@ pub enum Op {
Strip, Strip,
} }
impl Op {
/// maps an operator to its overload method name.
pub fn overload_method(self) -> Option<&'static str> {
Some(match self {
Op::Add => "add",
Op::Sub => "sub",
Op::Mul => "mul",
Op::Div => "div",
Op::Mod => "rem",
Op::Pow => "pow",
Op::Eq => "eq",
Op::Neq => "ne",
Op::Lt => "lt",
Op::Gt => "gt",
Op::Lte => "le",
Op::Gte => "ge",
Op::Neg => "neg",
Op::Not => "not",
Op::Strip => "strip",
Op::And | Op::Or => return None,
})
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Stmt { pub enum Stmt {
Let(String, Option<String>, Expr), Let(String, Option<String>, Expr),

View File

@ -23,6 +23,11 @@ impl Interpreter {
let l_raw = self.eval_expr(lhs, depth)?; let l_raw = self.eval_expr(lhs, depth)?;
let r_raw = self.eval_expr(rhs, depth)?; let r_raw = self.eval_expr(rhs, depth)?;
if let Some(res) = self.try_binop_overload(*op, &l_raw, &r_raw, depth) {
return res;
}
let (l, l_unit) = unwrap_spice(&l_raw); let (l, l_unit) = unwrap_spice(&l_raw);
let (r, r_unit) = unwrap_spice(&r_raw); let (r, r_unit) = unwrap_spice(&r_raw);
let had_unit = l_unit.is_some() || r_unit.is_some(); let had_unit = l_unit.is_some() || r_unit.is_some();
@ -99,4 +104,56 @@ impl Interpreter {
}; };
Ok(retag_spice(result?, unit_after)) Ok(retag_spice(result?, unit_after))
} }
/// dispatches a binary operator to a typed operand's impl method or an extern hook.
fn try_binop_overload(&mut self, op: Op, left: &Value, right: &Value, depth: u32)
-> Option<Result<Value, String>>
{
if let Value::Struct(s) = left {
let tag = s.borrow().get("__type").and_then(|v| match v {
Value::Str(s) => Some(s.clone()),
_ => None,
});
if let (Some(tag), Some(method)) = (tag, op.overload_method()) {
if let Some(fndef) = self.methods.get(&(tag, method.to_string())).cloned() {
return Some(self.call_fndef(&fndef, &[left.clone(), right.clone()], depth));
}
}
}
if matches!(left, Value::Extern(_)) || matches!(right, Value::Extern(_)) {
let hooks = self.hooks.hooks.clone();
for h in &hooks {
if let Some(res) = h.extern_binop(self, op, left, right, depth) {
return Some(res);
}
}
}
None
}
/// dispatches a unary operator to a typed operand's impl method or an extern hook.
pub(crate) fn try_unop_overload(&mut self, op: Op, operand: &Value, depth: u32)
-> Option<Result<Value, String>>
{
if let Value::Struct(s) = operand {
let tag = s.borrow().get("__type").and_then(|v| match v {
Value::Str(s) => Some(s.clone()),
_ => None,
});
if let (Some(tag), Some(method)) = (tag, op.overload_method()) {
if let Some(fndef) = self.methods.get(&(tag, method.to_string())).cloned() {
return Some(self.call_fndef(&fndef, &[operand.clone()], depth));
}
}
}
if matches!(operand, Value::Extern(_)) {
let hooks = self.hooks.hooks.clone();
for h in &hooks {
if let Some(res) = h.extern_unop(self, op, operand, depth) {
return Some(res);
}
}
}
None
}
} }

View File

@ -58,6 +58,9 @@ impl Interpreter {
} }
Expr::UnaryOp(Op::Neg, inner) => { Expr::UnaryOp(Op::Neg, inner) => {
let v = self.eval_expr(inner, depth)?; let v = self.eval_expr(inner, depth)?;
if let Some(res) = self.try_unop_overload(Op::Neg, &v, depth) {
return res;
}
match v { match v {
Value::Number(n) => Ok(Value::Number(-n)), Value::Number(n) => Ok(Value::Number(-n)),
_ => Err("cannot negate non-number".into()), _ => Err("cannot negate non-number".into()),

View File

@ -1,7 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;
use super::ast::{Expr, Stmt}; use super::ast::{Expr, Op, Stmt};
use super::parse::Parser; use super::parse::Parser;
use super::token::Token; use super::token::Token;
use super::value::{ExternHandle, Value}; use super::value::{ExternHandle, Value};
@ -34,6 +34,11 @@ pub trait InterpreterHook {
fn extern_method_call(&self, _i: &mut Interpreter, _h: &ExternHandle, fn extern_method_call(&self, _i: &mut Interpreter, _h: &ExternHandle,
_method: &str, _args: &[Value], _depth: u32) -> Option<Result<Value, String>> { None } _method: &str, _args: &[Value], _depth: u32) -> Option<Result<Value, String>> { None }
fn extern_eq(&self, _a: &ExternHandle, _b: &ExternHandle) -> Option<bool> { None } fn extern_eq(&self, _a: &ExternHandle, _b: &ExternHandle) -> Option<bool> { None }
fn extern_binop(&self, _i: &mut Interpreter, _op: Op, _left: &Value, _right: &Value, _depth: u32)
-> Option<Result<Value, String>> { None }
fn extern_unop(&self, _i: &mut Interpreter, _op: Op, _operand: &Value, _depth: u32)
-> Option<Result<Value, String>> { None }
} }
pub(crate) struct HookList { pub(crate) struct HookList {

View File

@ -28,13 +28,12 @@ pub use modules::{extract_use_declarations, 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};
pub use token::Token; pub use token::{tokenize, Token};
pub use value::{display_value_with_type, ExternHandle, Value}; pub use value::{display_value_with_type, ExternHandle, Value};
use hooks::HookList; use hooks::HookList;
use spice::source_enables_spice; use spice::source_enables_spice;
use tables::{coerce_cell_value, rows_to_value}; use tables::{coerce_cell_value, rows_to_value};
use token::tokenize;
pub(crate) const MAX_ITERATIONS: usize = 10_000; pub(crate) const MAX_ITERATIONS: usize = 10_000;
pub(crate) const MAX_CALL_DEPTH: u32 = 256; pub(crate) const MAX_CALL_DEPTH: u32 = 256;

View File

@ -110,7 +110,6 @@ fn use_module_colon_submodule_loads_sibling_cord_file() {
assert!(matches!(i.get_var("standard_outer"), Some(Value::Number(n)) if (n - 9.8).abs() < 1e-9)); assert!(matches!(i.get_var("standard_outer"), Some(Value::Number(n)) if (n - 9.8).abs() < 1e-9));
let v = i.eval("make_toroid()").unwrap(); let v = i.eval("make_toroid()").unwrap();
assert!(matches!(v, Value::Number(n) if n == 42.0)); assert!(matches!(v, Value::Number(n) if n == 42.0));
// coils.cord should NOT be loaded by `use sitter::primitives`
assert!(i.get_var("standard_turns").is_none()); assert!(i.get_var("standard_turns").is_none());
fs::remove_dir_all(&dir).ok(); fs::remove_dir_all(&dir).ok();

View File

@ -24,3 +24,4 @@ mod embedding;
mod hooks; mod hooks;
mod loader; mod loader;
mod module_paths; mod module_paths;
mod operators;

View File

@ -0,0 +1,145 @@
use std::rc::Rc;
use crate::interp::*;
#[allow(unused_imports)]
use super::helpers::*;
#[test]
fn struct_add_overload_dispatches_to_impl() {
let mut i = Interpreter::new();
i.exec("impl Vec2 { fn new(x, y) { return {__type: \"Vec2\", x: x, y: y} } fn add(self, other) { return Vec2::new(self.x + other.x, self.y + other.y) } }").unwrap();
i.exec("let a = Vec2::new(1, 2)").unwrap();
i.exec("let b = Vec2::new(3, 4)").unwrap();
let v = i.eval("(a + b).x").unwrap();
assert!(matches!(v, Value::Number(n) if n == 4.0));
let v = i.eval("(a + b).y").unwrap();
assert!(matches!(v, Value::Number(n) if n == 6.0));
}
#[test]
fn struct_mul_overload_dispatches() {
let mut i = Interpreter::new();
i.exec("impl Scale { fn new(f) { return {__type: \"Scale\", f: f} } fn mul(self, other) { return Scale::new(self.f * other.f) } }").unwrap();
i.exec("let a = Scale::new(3)").unwrap();
i.exec("let b = Scale::new(4)").unwrap();
let v = i.eval("(a * b).f").unwrap();
assert!(matches!(v, Value::Number(n) if n == 12.0));
}
#[test]
fn struct_eq_overload_overrides_default_false() {
let mut i = Interpreter::new();
i.exec("impl Tag { fn new(id) { return {__type: \"Tag\", id: id} } fn eq(self, other) { return self.id == other.id } }").unwrap();
i.exec("let a = Tag::new(7)").unwrap();
i.exec("let b = Tag::new(7)").unwrap();
let v = i.eval("a == b").unwrap();
assert!(matches!(v, Value::Bool(true)));
}
#[test]
fn struct_neg_overload_dispatches() {
let mut i = Interpreter::new();
i.exec("impl Money { fn new(c) { return {__type: \"Money\", c: c} } fn neg(self) { return Money::new(-self.c) } }").unwrap();
i.exec("let m = Money::new(5)").unwrap();
let v = i.eval("(-m).c").unwrap();
assert!(matches!(v, Value::Number(n) if n == -5.0));
}
#[test]
fn untyped_struct_without_overload_keeps_default_behavior() {
let mut i = Interpreter::new();
i.exec("let a = {x: 1}").unwrap();
i.exec("let b = {x: 1}").unwrap();
let v = i.eval("a == b").unwrap();
assert!(matches!(v, Value::Bool(false)));
}
struct VecExtern;
impl InterpreterHook for VecExtern {
fn name(&self) -> &str { "vec_extern" }
fn extern_binop(&self, _i: &mut Interpreter, op: Op, left: &Value, right: &Value, _depth: u32)
-> Option<Result<Value, String>>
{
let (a, b) = match (left, right) {
(Value::Extern(a), Value::Extern(b)) if a.kind == "vec" && b.kind == "vec" => (a.id, b.id),
_ => return None,
};
match op {
Op::Add => Some(Ok(Value::Number((a + b) as f64))),
Op::Eq => Some(Ok(Value::Bool(a == b))),
_ => None,
}
}
fn extern_unop(&self, _i: &mut Interpreter, op: Op, operand: &Value, _depth: u32)
-> Option<Result<Value, String>>
{
let id = match operand { Value::Extern(h) if h.kind == "vec" => h.id, _ => return None };
match op {
Op::Neg => Some(Ok(Value::Number(-(id as f64)))),
_ => None,
}
}
}
#[test]
fn extern_binop_hook_handles_addition() {
let mut i = Interpreter::new();
i.register_hook(Rc::new(VecExtern)).unwrap();
i.set_var("u", Value::Extern(ExternHandle { kind: "vec".into(), id: 3 }));
i.set_var("v", Value::Extern(ExternHandle { kind: "vec".into(), id: 4 }));
let r = i.eval("u + v").unwrap();
assert!(matches!(r, Value::Number(n) if n == 7.0));
}
#[test]
fn extern_binop_hook_handles_equality() {
let mut i = Interpreter::new();
i.register_hook(Rc::new(VecExtern)).unwrap();
i.set_var("u", Value::Extern(ExternHandle { kind: "vec".into(), id: 5 }));
i.set_var("v", Value::Extern(ExternHandle { kind: "vec".into(), id: 5 }));
let r = i.eval("u == v").unwrap();
assert!(matches!(r, Value::Bool(true)));
}
#[test]
fn extern_unop_hook_handles_negation() {
let mut i = Interpreter::new();
i.register_hook(Rc::new(VecExtern)).unwrap();
i.set_var("u", Value::Extern(ExternHandle { kind: "vec".into(), id: 9 }));
let r = i.eval("-u").unwrap();
assert!(matches!(r, Value::Number(n) if n == -9.0));
}
#[test]
fn extern_binop_falls_through_when_hook_declines() {
let mut i = Interpreter::new();
i.register_hook(Rc::new(VecExtern)).unwrap();
i.set_var("u", Value::Extern(ExternHandle { kind: "other".into(), id: 1 }));
i.set_var("v", Value::Extern(ExternHandle { kind: "other".into(), id: 2 }));
let r = i.eval("u + v");
assert!(r.is_err());
}
#[test]
fn module_reload_picks_up_changed_file_without_restart() {
use std::fs;
let dir = std::env::temp_dir().join(format!("acord-reload-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let file = dir.join("conf.cord");
let mut i = Interpreter::new();
i.add_module_path("conf", &file);
fs::write(&file, "let setting = 1").unwrap();
i.exec("use conf").unwrap();
assert!(matches!(i.get_var("setting"), Some(Value::Number(n)) if n == 1.0));
fs::write(&file, "let setting = 2").unwrap();
i.exec("use conf").unwrap();
assert!(matches!(i.get_var("setting"), Some(Value::Number(n)) if n == 2.0));
fs::remove_dir_all(&dir).ok();
}

View File

@ -111,7 +111,7 @@ fn finalize_number(
} }
} }
pub(crate) fn tokenize(input: &str, spice: bool) -> Result<Vec<Token>, String> { pub fn tokenize(input: &str, spice: bool) -> Result<Vec<Token>, String> {
let mut tokens = Vec::new(); let mut tokens = Vec::new();
let chars: Vec<char> = input.chars().collect(); let chars: Vec<char> = input.chars().collect();
let len = chars.len(); let len = chars.len();