diff --git a/compile/src/lib.rs b/compile/src/lib.rs index 262fa1f..1192ddb 100644 --- a/compile/src/lib.rs +++ b/compile/src/lib.rs @@ -49,6 +49,11 @@ pub fn decompose(source: &str) -> Result { /// decomposes with a custom hook for external extensions. pub fn decompose_with(source: &str, hook: &dyn DecomposeHook) -> Result { 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); @@ -56,18 +61,28 @@ pub fn decompose_with(source: &str, hook: &dyn DecomposeHook) -> Result, hook: &dyn DecomposeHook) -> Result<(), String> { let mut uses = Vec::new(); let mut fns = Vec::new(); + let mut impls = Vec::new(); let mut rest = Vec::new(); for s in stmts { match s { Stmt::Use(..) => uses.push(s), Stmt::FnDef { .. } => fns.push(s), + Stmt::ImplBlock { .. } | Stmt::TraitDef { .. } => impls.push(s), _ => rest.push(s), } } @@ -81,15 +96,77 @@ fn emit_program(out: &mut String, stmts: &[Stmt], deps: &mut Vec, ho emit_stmt(out, s, 0, deps, hook)?; out.push('\n'); } + + let mut regs: Vec = 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 {"); for s in &rest { emit_stmt(out, s, 1, deps, hook)?; } indent(out, 1, "V::Void"); indent(out, 0, "}"); + out.push('\n'); + + emit_dispatch(out, ®s); 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, + deps: &mut Vec, + 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::>() + .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 {\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::>() + .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, hook: &dyn DecomposeHook) -> Result<(), String> { if let Some(custom) = hook.stmt(stmt) { indent(out, depth, &custom); @@ -194,24 +271,8 @@ fn emit_stmt(out: &mut String, stmt: &Stmt, depth: usize, deps: &mut Vec { - indent(out, depth, &format!("pub trait {} {{", ident(name))); - 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, "}"); - } + // hoisted to free functions in emit_program. + Stmt::TraitDef { .. } | Stmt::ImplBlock { .. } => {} } Ok(()) } @@ -344,6 +405,7 @@ fn indent(out: &mut String, depth: usize, line: &str) { } fn ident(name: &str) -> String { + if name == "self" { return "self_".into(); } if RESERVED.contains(&name) { format!("r#{}", name) } else { name.replace('-', "_") } } @@ -405,25 +467,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_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_not(a: &V) -> V { V::Bool(!v_truthy(a)) } -fn v_strip(a: &V) -> V { match a { V::Str(s) => V::Str(s.trim().into()), _ => a.clone() } } +fn v_struct_type(v: &V) -> Option { + if let V::Struct(m) = v { + 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_sub(a: &V, b: &V) -> V { V::Num(v_num(a) - v_num(b)) } -fn v_mul(a: &V, b: &V) -> V { V::Num(v_num(a) * v_num(b)) } -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_pow(a: &V, b: &V) -> V { V::Num(v_num(a).powf(v_num(b))) } +fn v_binop_overload(method: &str, a: &V, b: &V) -> Option { + if let Some(t) = v_struct_type(a) { return v_dispatch(&t, method, &[a.clone(), b.clone()]); } + None +} +fn v_unop_overload(method: &str, a: &V) -> Option { + 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_neq(a: &V, b: &V) -> V { V::Bool(a != b) } -fn v_lt(a: &V, b: &V) -> V { V::Bool(v_num(a) < v_num(b)) } -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_gte(a: &V, b: &V) -> V { V::Bool(v_num(a) >= v_num(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_not(a: &V) -> V { if let Some(r) = v_unop_overload("not", a) { return r; } V::Bool(!v_truthy(a)) } +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_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_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_or(a: &V, b: &V) -> V { V::Bool(v_truthy(a) || v_truthy(b)) } @@ -484,7 +562,14 @@ 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_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_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 --- @@ -616,4 +701,79 @@ mod tests { assert!(r.code.contains("use 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 mut code = dec(src); + 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 _ = 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(); + } } diff --git a/core/src/interp/ast.rs b/core/src/interp/ast.rs index c502e3f..43815f7 100644 --- a/core/src/interp/ast.rs +++ b/core/src/interp/ast.rs @@ -1,4 +1,4 @@ -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Op { Add, Sub, Mul, Div, Mod, Pow, Eq, Neq, Lt, Gt, Lte, Gte, @@ -6,6 +6,30 @@ pub enum Op { 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)] pub enum Stmt { Let(String, Option, Expr), diff --git a/core/src/interp/eval/binop.rs b/core/src/interp/eval/binop.rs index b346f53..bc3855b 100644 --- a/core/src/interp/eval/binop.rs +++ b/core/src/interp/eval/binop.rs @@ -23,6 +23,11 @@ impl Interpreter { let l_raw = self.eval_expr(lhs, 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 (r, r_unit) = unwrap_spice(&r_raw); let had_unit = l_unit.is_some() || r_unit.is_some(); @@ -99,4 +104,56 @@ impl Interpreter { }; 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> + { + 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> + { + 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 + } } diff --git a/core/src/interp/eval/expr.rs b/core/src/interp/eval/expr.rs index c1ea211..9f207d0 100644 --- a/core/src/interp/eval/expr.rs +++ b/core/src/interp/eval/expr.rs @@ -58,6 +58,9 @@ impl Interpreter { } Expr::UnaryOp(Op::Neg, inner) => { let v = self.eval_expr(inner, depth)?; + if let Some(res) = self.try_unop_overload(Op::Neg, &v, depth) { + return res; + } match v { Value::Number(n) => Ok(Value::Number(-n)), _ => Err("cannot negate non-number".into()), diff --git a/core/src/interp/hooks.rs b/core/src/interp/hooks.rs index 03fb543..d2a8aac 100644 --- a/core/src/interp/hooks.rs +++ b/core/src/interp/hooks.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::rc::Rc; -use super::ast::{Expr, Stmt}; +use super::ast::{Expr, Op, Stmt}; use super::parse::Parser; use super::token::Token; use super::value::{ExternHandle, Value}; @@ -34,6 +34,11 @@ pub trait InterpreterHook { fn extern_method_call(&self, _i: &mut Interpreter, _h: &ExternHandle, _method: &str, _args: &[Value], _depth: u32) -> Option> { None } fn extern_eq(&self, _a: &ExternHandle, _b: &ExternHandle) -> Option { None } + + fn extern_binop(&self, _i: &mut Interpreter, _op: Op, _left: &Value, _right: &Value, _depth: u32) + -> Option> { None } + fn extern_unop(&self, _i: &mut Interpreter, _op: Op, _operand: &Value, _depth: u32) + -> Option> { None } } pub(crate) struct HookList { diff --git a/core/src/interp/mod.rs b/core/src/interp/mod.rs index 8b28995..737f44c 100644 --- a/core/src/interp/mod.rs +++ b/core/src/interp/mod.rs @@ -28,13 +28,12 @@ pub use modules::{extract_use_declarations, ModuleExports, UseDecl}; pub use parse::{parse_program, Parser}; pub use ring::RingBuf; 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}; use hooks::HookList; use spice::source_enables_spice; use tables::{coerce_cell_value, rows_to_value}; -use token::tokenize; pub(crate) const MAX_ITERATIONS: usize = 10_000; pub(crate) const MAX_CALL_DEPTH: u32 = 256; diff --git a/core/src/interp/tests/loader.rs b/core/src/interp/tests/loader.rs index 2893b07..c075527 100644 --- a/core/src/interp/tests/loader.rs +++ b/core/src/interp/tests/loader.rs @@ -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)); let v = i.eval("make_toroid()").unwrap(); 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()); fs::remove_dir_all(&dir).ok(); diff --git a/core/src/interp/tests/mod.rs b/core/src/interp/tests/mod.rs index a7b3967..e9110de 100644 --- a/core/src/interp/tests/mod.rs +++ b/core/src/interp/tests/mod.rs @@ -24,3 +24,4 @@ mod embedding; mod hooks; mod loader; mod module_paths; +mod operators; diff --git a/core/src/interp/tests/operators.rs b/core/src/interp/tests/operators.rs new file mode 100644 index 0000000..07dbbc0 --- /dev/null +++ b/core/src/interp/tests/operators.rs @@ -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> + { + 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> + { + 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(); +} diff --git a/core/src/interp/token.rs b/core/src/interp/token.rs index 444afeb..7c3b692 100644 --- a/core/src/interp/token.rs +++ b/core/src/interp/token.rs @@ -111,7 +111,7 @@ fn finalize_number( } } -pub(crate) fn tokenize(input: &str, spice: bool) -> Result, String> { +pub fn tokenize(input: &str, spice: bool) -> Result, String> { let mut tokens = Vec::new(); let chars: Vec = input.chars().collect(); let len = chars.len();