added 2 arg trig ops

This commit is contained in:
jess 2026-06-09 22:29:37 -07:00
parent b1943d1a4f
commit d326931dc2
4 changed files with 145 additions and 3 deletions

View File

@ -364,6 +364,7 @@ fn collect_lvalue_path(expr: &Expr, steps: &mut Vec<String>, cx: &Ctx) -> Result
/// builtin names lowered to a single runtime dispatch call.
const VALUE_BUILTINS: &[&str] = &[
"sin", "cos", "tan", "asin", "acos", "atan", "sqrt", "abs", "ln", "log",
"atan2", "hypot", "pow", "copysign", "fmod",
"floor", "ceil", "round", "rand", "seed",
"sum", "avg", "min", "max", "count", "std_devp", "std_devs",
"len", "range", "push",
@ -954,10 +955,15 @@ fn v_builtin_call(name: &str, a: &[V]) -> Option<V> {
"asin" => V::Num(v_num(&v_arg(a, 0)).asin()),
"acos" => V::Num(v_num(&v_arg(a, 0)).acos()),
"atan" => V::Num(v_num(&v_arg(a, 0)).atan()),
"atan2" => V::Num(v_num(&v_arg(a, 0)).atan2(v_num(&v_arg(a, 1)))),
"hypot" => V::Num(v_num(&v_arg(a, 0)).hypot(v_num(&v_arg(a, 1)))),
"pow" => { let x = v_num(&v_arg(a, 0)); let y = if a.len() >= 2 { v_num(&v_arg(a, 1)) } else { 2.0 }; V::Num(x.powf(y)) }
"copysign" => { let x = v_num(&v_arg(a, 0)); let y = if a.len() >= 2 { v_num(&v_arg(a, 1)) } else { 1.0 }; V::Num(x.copysign(y)) }
"fmod" => { let x = v_num(&v_arg(a, 0)); let y = if a.len() >= 2 { v_num(&v_arg(a, 1)) } else { 1.0 }; V::Num(x % y) }
"sqrt" => V::Num(v_num(&v_arg(a, 0)).sqrt()),
"abs" => V::Num(v_num(&v_arg(a, 0)).abs()),
"ln" => V::Num(v_num(&v_arg(a, 0)).ln()),
"log" => V::Num(v_num(&v_arg(a, 0)).log10()),
"log" => { let x = v_num(&v_arg(a, 0)); V::Num(if a.len() >= 2 { x.log(v_num(&v_arg(a, 1))) } else { x.log10() }) }
"floor" | "ceil" | "round" => { let n = v_num(&v_arg(a, 0)); let digits = if a.len() >= 2 { v_num(&v_arg(a, 1)) as i32 } else { 0 }; let f = 10f64.powi(digits); let s = n * f; V::Num(match name { "floor" => s.floor() / f, "ceil" => s.ceil() / f, _ => s.round() / f }) }
"rand" => v_rand(a),
"seed" => { v_rng_state(Some(v_num(&v_arg(a, 0)).to_bits())); V::Void }
@ -1062,6 +1068,26 @@ mod tests {
assert!(out.contains("v_field("));
}
#[test]
fn two_arg_math_builtins_lower_to_dispatch() {
// every new 2-arg math builtin must route through the runtime dispatcher,
// keeping the compiled prelude in parity with the interpreter.
for src in [
"let r = atan2(1, 1)",
"let r = hypot(3, 4)",
"let r = pow(2, 8)",
"let r = pow(5)",
"let r = copysign(3, -1)",
"let r = fmod(7, 3)",
"let r = log(8, 2)",
] {
let out = dec(src);
assert!(out.contains("v_builtin_call("), "`{src}` did not route through v_builtin_call:\n{out}");
}
assert!(dec("let r = atan2(1, 1)").contains("v_builtin_call(\"atan2\""));
assert!(dec("let r = hypot(3, 4)").contains("v_builtin_call(\"hypot\""));
}
#[test]
fn reserved_word_raw_ident() {
let out = dec("let type = 1");

View File

@ -12,7 +12,9 @@ pub(crate) fn try_call(
) -> Option<Result<Value, String>> {
match name {
"sin" | "cos" | "tan" | "asin" | "acos" | "atan"
| "sqrt" | "abs" | "ln" | "log" => Some(call_transcendental(interp, name, args, depth)),
| "sqrt" | "abs" | "ln" => Some(call_transcendental(interp, name, args, depth)),
"atan2" | "hypot" | "pow" | "log" | "copysign" | "fmod"
=> Some(call_binary(interp, name, args, depth)),
"floor" | "ceil" | "round" => Some(call_rounding(interp, name, args, depth)),
_ => None,
}
@ -43,7 +45,56 @@ fn call_transcendental(
"sqrt" => n.sqrt(),
"abs" => n.abs(),
"ln" => n.ln(),
"log" => n.log10(),
_ => unreachable!(),
};
Ok(retag_spice(Value::Number(result), unit))
}
/// two-argument math builtins. atan2/hypot require both arguments; pow/log/copysign/fmod
/// take an optional second argument that falls back to a per-function default.
fn call_binary(
interp: &mut Interpreter,
name: &str,
args: &[Expr],
depth: u32,
) -> Result<Value, String> {
let two_required = matches!(name, "atan2" | "hypot");
if two_required && args.len() != 2 {
return Err(format!("{}() expects 2 arguments", name));
}
if !two_required && (args.is_empty() || args.len() > 2) {
return Err(format!("{}() expects 1 or 2 arguments", name));
}
let v = interp.eval_expr(&args[0], depth)?;
let (raw, unit) = unwrap_spice(&v);
let a = match raw {
Value::Number(n) => n,
_ => return Err(format!("{}() expects a number", name)),
};
// second argument, defaulting per-function when omitted.
let b = if args.len() == 2 {
let v2 = interp.eval_expr(&args[1], depth)?;
let (raw2, _) = unwrap_spice(&v2);
match raw2 {
Value::Number(n) => n,
_ => return Err(format!("{}() expects a number", name)),
}
} else {
match name {
"pow" => 2.0,
"log" => 10.0,
"copysign" | "fmod" => 1.0,
_ => unreachable!(),
}
};
let result = match name {
"atan2" => a.atan2(b),
"hypot" => a.hypot(b),
"pow" => a.powf(b),
// log10 keeps full precision for the implicit base.
"log" => if args.len() == 2 { a.log(b) } else { a.log10() },
"copysign" => a.copysign(b),
"fmod" => a % b,
_ => unreachable!(),
};
Ok(retag_spice(Value::Number(result), unit))

View File

@ -11,6 +11,70 @@ use super::helpers::*;
assert_eq!(eval_one("sqrt(16)"), "4");
}
#[test]
fn two_arg_trig_atan2_and_hypot() {
let mut i = Interpreter::new();
// atan2(y, x): angle of (1, 1) is pi/4.
let v = i.eval("atan2(1, 1)").unwrap();
match v { Value::Number(n) => assert!(approx(n, std::f64::consts::FRAC_PI_4)), _ => panic!() }
// UFCS form mirrors the free call: y.atan2(x) == atan2(y, x).
i.exec("let y = 1").unwrap();
let v = i.eval("y.atan2(1)").unwrap();
match v { Value::Number(n) => assert!(approx(n, std::f64::consts::FRAC_PI_4)), _ => panic!() }
// 3-4-5 triangle.
let v = i.eval("hypot(3, 4)").unwrap();
assert!(matches!(v, Value::Number(n) if n == 5.0));
i.exec("let a = 3").unwrap();
let v = i.eval("a.hypot(4)").unwrap();
assert!(matches!(v, Value::Number(n) if n == 5.0));
}
#[test]
fn atan2_and_hypot_require_two_args() {
let mut i = Interpreter::new();
assert!(i.eval("atan2(1)").is_err());
assert!(i.eval("hypot(3)").is_err());
}
#[test]
fn pow_with_optional_exponent() {
let mut i = Interpreter::new();
let v = i.eval("pow(2, 10)").unwrap();
assert!(matches!(v, Value::Number(n) if n == 1024.0));
// omitted exponent squares.
let v = i.eval("pow(5)").unwrap();
assert!(matches!(v, Value::Number(n) if n == 25.0));
i.exec("let b = 5").unwrap();
let v = i.eval("b.pow(3)").unwrap();
assert!(matches!(v, Value::Number(n) if n == 125.0));
}
#[test]
fn log_with_optional_base() {
let mut i = Interpreter::new();
// implicit base 10.
let v = i.eval("log(1000)").unwrap();
match v { Value::Number(n) => assert!(approx(n, 3.0)), _ => panic!() }
// explicit base.
let v = i.eval("log(8, 2)").unwrap();
match v { Value::Number(n) => assert!(approx(n, 3.0)), _ => panic!() }
}
#[test]
fn copysign_and_fmod_with_optional_second_arg() {
let mut i = Interpreter::new();
let v = i.eval("copysign(3, -1)").unwrap();
assert!(matches!(v, Value::Number(n) if n == -3.0));
// omitted sign source defaults positive.
let v = i.eval("copysign(-3)").unwrap();
assert!(matches!(v, Value::Number(n) if n == 3.0));
let v = i.eval("fmod(7, 3)").unwrap();
assert!(matches!(v, Value::Number(n) if n == 1.0));
// omitted divisor defaults to 1, yielding the fractional part.
let v = i.eval("fmod(2.75)").unwrap();
match v { Value::Number(n) => assert!(approx(n, 0.75)), _ => panic!() }
}
#[test]
fn round_with_digits() {
let mut i = Interpreter::new();

View File

@ -727,6 +727,7 @@ fn is_cordial_builtin(w: &str) -> bool {
matches!(w,
// math
"sin" | "cos" | "tan" | "asin" | "acos" | "atan"
| "atan2" | "hypot" | "pow" | "copysign" | "fmod"
| "sqrt" | "abs" | "floor" | "ceil" | "round" | "ln" | "log"
// collections
| "len" | "range" | "push"