extern crate alloc;
use crate::address::Address;
use crate::float::Float;
use crate::vm::GenericVm;
use crate::word::Word;
pub trait Library<W: Word, A: Address, F: Float> {
fn register<'a, 'arena>(self, vm: &mut GenericVm<'a, 'arena, W, A, F>);
}
pub struct Math;
pub struct Audio;
pub struct Shell;
impl Math {
pub const SIGNATURES: &'static str = concat!(
"use math::sqrt(Float) -> Float\n",
"use math::pow(Float, Float) -> Float\n",
"use math::abs(Float) -> Float\n",
"use math::sign(Float) -> Float\n",
"use math::floor(Float) -> Float\n",
"use math::ceil(Float) -> Float\n",
"use math::round(Float) -> Float\n",
"use math::trunc(Float) -> Float\n",
"use math::fmod(Float, Float) -> Float\n",
"use math::hypot(Float, Float) -> Float\n",
"use math::min(Float, Float) -> Float\n",
"use math::max(Float, Float) -> Float\n",
"use math::clamp(Float, Float, Float) -> Float\n",
"use math::lerp(Float, Float, Float) -> Float\n",
"use math::sin(Float) -> Float\n",
"use math::cos(Float) -> Float\n",
"use math::tan(Float) -> Float\n",
"use math::asin(Float) -> Float\n",
"use math::acos(Float) -> Float\n",
"use math::atan(Float) -> Float\n",
"use math::atan2(Float, Float) -> Float\n",
"use math::tanh(Float) -> Float\n",
"use math::exp(Float) -> Float\n",
"use math::ln(Float) -> Float\n",
"use math::log10(Float) -> Float\n",
"use math::log2(Float) -> Float\n",
"use math::pi() -> Float\n",
"use math::tau() -> Float\n",
"use math::e() -> Float\n",
"use math::sqrt_2() -> Float\n",
"use math::ln_2() -> Float\n",
"use math::ln_10() -> Float\n",
);
}
impl Audio {
pub const SIGNATURES: &'static str = concat!(
"use audio::midi_to_freq(Word) -> Float\n",
"use audio::freq_to_midi(Float) -> Word\n",
"use audio::cents_to_ratio(Float) -> Float\n",
"use audio::ratio_to_cents(Float) -> Float\n",
"use audio::semitones_to_ratio(Float) -> Float\n",
"use audio::ratio_to_semitones(Float) -> Float\n",
"use audio::db_to_linear(Float) -> Float\n",
"use audio::linear_to_db(Float) -> Float\n",
"use audio::ms_to_samples(Float, Float) -> Float\n",
"use audio::samples_to_ms(Float, Float) -> Float\n",
"use audio::onepole_lpf_alpha(Float, Float) -> Float\n",
"use audio::onepole_hpf_alpha(Float, Float) -> Float\n",
"use audio::pan_law(Float) -> (Float, Float)\n",
);
}
impl<W: Word, A: Address, F: Float> Library<W, A, F> for Math {
fn register<'a, 'arena>(self, vm: &mut GenericVm<'a, 'arena, W, A, F>) {
math::register(vm);
}
}
impl<W: Word, A: Address, F: Float> Library<W, A, F> for Audio {
fn register<'a, 'arena>(self, vm: &mut GenericVm<'a, 'arena, W, A, F>) {
crate::audio_natives::register_audio_natives(vm);
}
}
#[cfg(feature = "shell")]
impl<W: Word, A: Address, F: Float> Library<W, A, F> for Shell {
fn register<'a, 'arena>(self, vm: &mut GenericVm<'a, 'arena, W, A, F>) {
shell::register(vm);
}
}
mod math {
extern crate alloc;
use alloc::string::String;
use core::f64::consts;
use crate::address::Address;
use crate::float::Float;
use crate::vm::{GenericVm, VmError};
use crate::word::Word;
pub fn register<'a, 'arena, W: Word, A: Address, F: Float>(
vm: &mut GenericVm<'a, 'arena, W, A, F>,
) {
vm.register_fn("math::sqrt", |x: f64| -> f64 { libm::sqrt(x) });
vm.register_fn("math::pow", |base: f64, exp: f64| -> f64 {
libm::pow(base, exp)
});
vm.register_fn("math::abs", |x: f64| -> f64 { libm::fabs(x) });
vm.register_fn("math::sign", |x: f64| -> f64 {
if x.is_nan() {
f64::NAN
} else if x > 0.0 {
1.0
} else if x < 0.0 {
-1.0
} else {
0.0
}
});
vm.register_fn("math::floor", |x: f64| -> f64 { libm::floor(x) });
vm.register_fn("math::ceil", |x: f64| -> f64 { libm::ceil(x) });
vm.register_fn("math::round", |x: f64| -> f64 { libm::round(x) });
vm.register_fn("math::trunc", |x: f64| -> f64 { libm::trunc(x) });
vm.register_fn_fallible("math::fmod", |x: f64, y: f64| -> Result<f64, VmError> {
if y == 0.0 {
return Err(VmError::NativeError(String::from(
"math::fmod: divisor must be non-zero",
)));
}
Ok(libm::fmod(x, y))
});
vm.register_fn("math::hypot", |x: f64, y: f64| -> f64 { libm::hypot(x, y) });
vm.register_fn("math::min", |a: f64, b: f64| -> f64 { libm::fmin(a, b) });
vm.register_fn("math::max", |a: f64, b: f64| -> f64 { libm::fmax(a, b) });
vm.register_fn("math::clamp", |val: f64, min: f64, max: f64| -> f64 {
if val < min {
min
} else if val > max {
max
} else {
val
}
});
vm.register_fn("math::lerp", |a: f64, b: f64, t: f64| -> f64 {
a + (b - a) * t
});
vm.register_fn("math::sin", |x: f64| -> f64 { libm::sin(x) });
vm.register_fn("math::cos", |x: f64| -> f64 { libm::cos(x) });
vm.register_fn("math::tan", |x: f64| -> f64 { libm::tan(x) });
vm.register_fn_fallible("math::asin", |x: f64| -> Result<f64, VmError> {
if !(-1.0..=1.0).contains(&x) {
return Err(VmError::NativeError(String::from(
"math::asin: argument must lie in [-1, 1]",
)));
}
Ok(libm::asin(x))
});
vm.register_fn_fallible("math::acos", |x: f64| -> Result<f64, VmError> {
if !(-1.0..=1.0).contains(&x) {
return Err(VmError::NativeError(String::from(
"math::acos: argument must lie in [-1, 1]",
)));
}
Ok(libm::acos(x))
});
vm.register_fn("math::atan", |x: f64| -> f64 { libm::atan(x) });
vm.register_fn("math::atan2", |y: f64, x: f64| -> f64 { libm::atan2(y, x) });
vm.register_fn("math::tanh", |x: f64| -> f64 { libm::tanh(x) });
vm.register_fn("math::exp", |x: f64| -> f64 { libm::exp(x) });
vm.register_fn_fallible("math::ln", |x: f64| -> Result<f64, VmError> {
if x <= 0.0 {
return Err(VmError::NativeError(String::from(
"math::ln: argument must be strictly positive",
)));
}
Ok(libm::log(x))
});
vm.register_fn_fallible("math::log10", |x: f64| -> Result<f64, VmError> {
if x <= 0.0 {
return Err(VmError::NativeError(String::from(
"math::log10: argument must be strictly positive",
)));
}
Ok(libm::log10(x))
});
vm.register_fn_fallible("math::log2", |x: f64| -> Result<f64, VmError> {
if x <= 0.0 {
return Err(VmError::NativeError(String::from(
"math::log2: argument must be strictly positive",
)));
}
Ok(libm::log2(x))
});
vm.register_fn("math::pi", || -> f64 { consts::PI });
vm.register_fn("math::tau", || -> f64 { consts::TAU });
vm.register_fn("math::e", || -> f64 { consts::E });
vm.register_fn("math::sqrt_2", || -> f64 { consts::SQRT_2 });
vm.register_fn("math::ln_2", || -> f64 { consts::LN_2 });
vm.register_fn("math::ln_10", || -> f64 { consts::LN_10 });
}
}
#[cfg(feature = "shell")]
pub mod shell;
#[cfg(feature = "shell")]
impl Shell {
pub const SIGNATURES: &'static str = concat!(
"use shell::getenv(Text) -> Option<Text>\n",
"use shell::has_env(Text) -> bool\n",
"use shell::run(Text) -> (Word, Text)\n",
"use shell::run_full(Text) -> (Word, Text, Text)\n",
"use shell::run_checked(Text) -> Text\n",
"use shell::exit(Word) -> ()\n",
"use shell::sleep_ms(Word) -> ()\n",
"use shell::now_unix_ms() -> Word\n",
"use shell::read_file(Text) -> Text\n",
"use shell::write_file(Text, Text) -> ()\n",
"use shell::append_file(Text, Text) -> ()\n",
"use shell::file_exists(Text) -> bool\n",
"use shell::write_err(Text) -> ()\n",
"use shell::writeln_err(Text) -> ()\n",
"use shell::pid() -> Word\n",
"use shell::hostname() -> Text\n",
"use shell::arg_count() -> Word\n",
"use shell::arg(Word) -> Option<Text>\n",
"use shell::setenv(Text, Text) -> ()\n",
"use shell::pwd() -> Text\n",
"use shell::cd(Text) -> ()\n",
"use shell::run_timeout(Text, Word) -> (Word, Text)\n",
);
}
#[cfg(all(test, feature = "compile", feature = "verify"))]
mod tests {
use super::*;
use crate::bytecode::Value;
use crate::compiler::compile;
use crate::lexer::tokenize;
use crate::parser::parse;
use crate::vm::{DEFAULT_ARENA_CAPACITY, Vm, VmState};
fn run_with_math(src: &str) -> Value {
let tokens = tokenize(src).expect("lex error");
let program = parse(&tokens).expect("parse error");
let module = compile(&program).expect("compile error");
let arena = keleusma_arena::Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).unwrap();
vm.register_library(Math);
match vm.call(&[]).unwrap() {
VmState::Finished(v) => v,
VmState::Yielded(v) => panic!("unexpected yield: {:?}", v),
VmState::Reset => panic!("unexpected reset"),
VmState::BreakpointHit { chunk, op } => {
panic!("unexpected breakpoint at chunk {} op {}", chunk, op)
}
}
}
fn assert_close(val: Value, expected: f64, tol: f64) {
match val {
Value::Float(f) => assert!(
(f - expected).abs() < tol,
"expected ~{}, got {}",
expected,
f
),
other => panic!("expected Float, got {:?}", other),
}
}
#[test]
fn math_sqrt() {
assert_close(
run_with_math("use math::sqrt\nfn main() -> Float { math::sqrt(9.0) }"),
3.0,
1e-9,
);
}
#[test]
fn math_pow() {
assert_close(
run_with_math("use math::pow\nfn main() -> Float { math::pow(2.0, 10.0) }"),
1024.0,
1e-9,
);
}
#[test]
fn math_abs() {
assert_close(
run_with_math("use math::abs\nfn main() -> Float { math::abs(-3.25) }"),
3.25,
1e-9,
);
}
#[test]
fn math_sign_positive() {
assert_close(
run_with_math("use math::sign\nfn main() -> Float { math::sign(7.5) }"),
1.0,
1e-9,
);
}
#[test]
fn math_sign_negative() {
assert_close(
run_with_math("use math::sign\nfn main() -> Float { math::sign(-0.001) }"),
-1.0,
1e-9,
);
}
#[test]
fn math_sign_zero() {
assert_close(
run_with_math("use math::sign\nfn main() -> Float { math::sign(0.0) }"),
0.0,
1e-12,
);
}
#[test]
fn math_floor() {
assert_close(
run_with_math("use math::floor\nfn main() -> Float { math::floor(3.7) }"),
3.0,
1e-9,
);
}
#[test]
fn math_ceil() {
assert_close(
run_with_math("use math::ceil\nfn main() -> Float { math::ceil(3.2) }"),
4.0,
1e-9,
);
}
#[test]
fn math_round() {
assert_close(
run_with_math("use math::round\nfn main() -> Float { math::round(3.5) }"),
4.0,
1e-9,
);
}
#[test]
fn math_trunc_positive() {
assert_close(
run_with_math("use math::trunc\nfn main() -> Float { math::trunc(3.7) }"),
3.0,
1e-9,
);
}
#[test]
fn math_trunc_negative() {
assert_close(
run_with_math("use math::trunc\nfn main() -> Float { math::trunc(-3.7) }"),
-3.0,
1e-9,
);
}
#[test]
fn math_fmod() {
assert_close(
run_with_math("use math::fmod\nfn main() -> Float { math::fmod(7.5, 2.0) }"),
1.5,
1e-9,
);
}
#[test]
fn math_hypot() {
assert_close(
run_with_math("use math::hypot\nfn main() -> Float { math::hypot(3.0, 4.0) }"),
5.0,
1e-9,
);
}
#[test]
fn math_min_max_clamp_lerp() {
assert_close(
run_with_math(
"use math::min\nuse math::max\nfn main() -> Float { math::min(10.0, math::max(3.0, 5.0)) }",
),
5.0,
1e-9,
);
assert_close(
run_with_math("use math::clamp\nfn main() -> Float { math::clamp(5.0, 0.0, 1.0) }"),
1.0,
1e-9,
);
assert_close(
run_with_math("use math::lerp\nfn main() -> Float { math::lerp(0.0, 100.0, 0.25) }"),
25.0,
1e-9,
);
}
#[test]
fn math_sin_cos_tan() {
assert_close(
run_with_math("use math::sin\nfn main() -> Float { math::sin(0.0) }"),
0.0,
1e-9,
);
assert_close(
run_with_math("use math::cos\nfn main() -> Float { math::cos(0.0) }"),
1.0,
1e-9,
);
assert_close(
run_with_math("use math::tan\nfn main() -> Float { math::tan(0.0) }"),
0.0,
1e-9,
);
}
#[test]
fn math_asin_acos_atan() {
assert_close(
run_with_math("use math::asin\nfn main() -> Float { math::asin(1.0) }"),
core::f64::consts::FRAC_PI_2,
1e-9,
);
assert_close(
run_with_math("use math::acos\nfn main() -> Float { math::acos(0.0) }"),
core::f64::consts::FRAC_PI_2,
1e-9,
);
assert_close(
run_with_math("use math::atan\nfn main() -> Float { math::atan(1.0) }"),
core::f64::consts::FRAC_PI_4,
1e-9,
);
}
#[test]
fn math_atan2_quadrants() {
assert_close(
run_with_math("use math::atan2\nfn main() -> Float { math::atan2(1.0, 1.0) }"),
core::f64::consts::FRAC_PI_4,
1e-9,
);
assert_close(
run_with_math("use math::atan2\nfn main() -> Float { math::atan2(1.0, -1.0) }"),
3.0 * core::f64::consts::FRAC_PI_4,
1e-9,
);
}
#[test]
fn math_tanh_zero_and_large() {
assert_close(
run_with_math("use math::tanh\nfn main() -> Float { math::tanh(0.0) }"),
0.0,
1e-12,
);
assert_close(
run_with_math("use math::tanh\nfn main() -> Float { math::tanh(100.0) }"),
1.0,
1e-9,
);
}
#[test]
fn math_exp_zero_and_one() {
assert_close(
run_with_math("use math::exp\nfn main() -> Float { math::exp(0.0) }"),
1.0,
1e-9,
);
assert_close(
run_with_math("use math::exp\nfn main() -> Float { math::exp(1.0) }"),
core::f64::consts::E,
1e-9,
);
}
#[test]
fn math_ln_e() {
assert_close(
run_with_math("use math::ln\nfn main() -> Float { math::ln(2.718281828459045) }"),
1.0,
1e-12,
);
}
#[test]
fn math_log10_and_log2() {
assert_close(
run_with_math("use math::log10\nfn main() -> Float { math::log10(1000.0) }"),
3.0,
1e-9,
);
assert_close(
run_with_math("use math::log2\nfn main() -> Float { math::log2(8.0) }"),
3.0,
1e-9,
);
}
#[test]
fn math_asin_domain_error() {
let tokens = tokenize("use math::asin\nfn main() -> Float { math::asin(2.0) }").unwrap();
let program = parse(&tokens).unwrap();
let module = compile(&program).unwrap();
let arena = keleusma_arena::Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).unwrap();
vm.register_library(Math);
assert!(vm.call(&[]).is_err());
}
#[test]
fn math_ln_nonpositive_error() {
let tokens = tokenize("use math::ln\nfn main() -> Float { math::ln(0.0) }").unwrap();
let program = parse(&tokens).unwrap();
let module = compile(&program).unwrap();
let arena = keleusma_arena::Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).unwrap();
vm.register_library(Math);
assert!(vm.call(&[]).is_err());
}
#[test]
fn math_fmod_zero_divisor_error() {
let tokens =
tokenize("use math::fmod\nfn main() -> Float { math::fmod(1.0, 0.0) }").unwrap();
let program = parse(&tokens).unwrap();
let module = compile(&program).unwrap();
let arena = keleusma_arena::Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).unwrap();
vm.register_library(Math);
assert!(vm.call(&[]).is_err());
}
#[test]
fn math_pi() {
assert_close(
run_with_math("use math::pi\nfn main() -> Float { math::pi() }"),
core::f64::consts::PI,
1e-15,
);
}
#[test]
fn math_tau() {
assert_close(
run_with_math("use math::tau\nfn main() -> Float { math::tau() }"),
core::f64::consts::TAU,
1e-15,
);
}
#[test]
fn math_e_constant() {
assert_close(
run_with_math("use math::e\nfn main() -> Float { math::e() }"),
core::f64::consts::E,
1e-15,
);
}
#[test]
fn math_sqrt_2_constant() {
assert_close(
run_with_math("use math::sqrt_2\nfn main() -> Float { math::sqrt_2() }"),
core::f64::consts::SQRT_2,
1e-15,
);
}
#[test]
fn math_ln_2_constant() {
assert_close(
run_with_math("use math::ln_2\nfn main() -> Float { math::ln_2() }"),
core::f64::consts::LN_2,
1e-15,
);
}
#[test]
fn math_ln_10_constant() {
assert_close(
run_with_math("use math::ln_10\nfn main() -> Float { math::ln_10() }"),
core::f64::consts::LN_10,
1e-15,
);
}
}