added 2 arg trig ops
This commit is contained in:
parent
b1943d1a4f
commit
d326931dc2
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue