geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
use super::parse::{format, parse_literal};
use super::schema::{ParseError, ParseOk, ParseResult};
use crate::gleam_stdlib::{GleamStdlibHostProfile, GleamStdlibRunState};
use crate::{HostCall, HostCallCompletion, HostCallError, HostFailure, HostProvider};
use ecow::EcoString;
use num_bigint::BigInt;
use num_traits::{FromPrimitive, ToPrimitive};
use std::marker::PhantomData;

pub(super) struct FloatProvider<Profile>(PhantomData<Profile>);

impl<Profile> HostProvider<Profile> for FloatProvider<Profile>
where
    Profile: GleamStdlibHostProfile,
{
    type State = GleamStdlibRunState;

    fn project(state: &mut Profile::RunState) -> &mut Self::State {
        Profile::gleam_stdlib_run_state(state)
    }
}

pub(super) fn parse<'call, Profile>(
    call: HostCall<'call, Profile, FloatProvider<Profile>, ParseResult>,
    source: EcoString,
) -> Result<HostCallCompletion<'call, ParseResult>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    Ok(match parse_literal(&source) {
        Some(value) => call.return_custom::<ParseOk>((value, ())),
        None => call.return_custom::<ParseError>(((), ())),
    })
}

pub(super) fn to_string(value: f64) -> EcoString {
    format(value)
}

pub(super) fn ceiling(value: f64) -> f64 {
    value.ceil()
}

pub(super) fn floor(value: f64) -> f64 {
    value.floor()
}

pub(super) fn js_round(value: f64) -> Result<BigInt, HostFailure> {
    BigInt::from_f64((value + 0.5).floor())
        .ok_or_else(|| HostFailure::new("float cannot be represented as an Int"))
}

pub(super) fn truncate(value: f64) -> Result<BigInt, HostFailure> {
    BigInt::from_f64(value.trunc())
        .ok_or_else(|| HostFailure::new("float cannot be represented as an Int"))
}

pub(in crate::gleam_stdlib) fn do_to_float(value: BigInt) -> Result<f64, HostFailure> {
    value
        .to_f64()
        .filter(|value| value.is_finite())
        .ok_or_else(|| HostFailure::new("Int cannot be represented as a finite Float"))
}

pub(super) fn do_power(base: f64, exponent: f64) -> f64 {
    base.powf(exponent)
}

pub(super) fn random<'call, Profile>(
    mut call: HostCall<'call, Profile, FloatProvider<Profile>, f64>,
) -> Result<HostCallCompletion<'call, f64>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let value = call.state().random_float();
    Ok(call.return_value(value))
}

pub(super) fn do_log(value: f64) -> f64 {
    value.ln()
}

pub(super) fn exponential(value: f64) -> f64 {
    value.exp()
}

#[cfg(test)]
mod tests {
    use super::super::host_provider;
    use super::{
        FloatProvider, ceiling, do_log, do_power, do_to_float, exponential, floor, js_round,
        to_string, truncate,
    };
    use crate::gleam_stdlib::{GleamStdlibProfile, GleamStdlibRunState};
    use crate::{
        HostModule, HostProvider, HostProviderSet, HostedExecution, ModuleSource, PackageSource,
        compile_typed_host_program, plan_host_program,
    };
    use ecow::EcoString;
    use num_bigint::BigInt;

    const FLOAT_DECLARATIONS: &str = r#"
@external(erlang, "gleam_stdlib", "parse_float")
pub fn parse(string: String) -> Result(Float, Nil)

@external(erlang, "gleam_stdlib", "float_to_string")
pub fn to_string(value: Float) -> String

@external(erlang, "math", "ceil")
pub fn ceiling(value: Float) -> Float

@external(erlang, "math", "floor")
pub fn floor(value: Float) -> Float

@external(erlang, "gleam_stdlib", "round")
@external(javascript, "../gleam_stdlib.mjs", "round")
pub fn js_round(value: Float) -> Int

@external(erlang, "erlang", "trunc")
pub fn truncate(value: Float) -> Int

@external(erlang, "erlang", "float")
pub fn do_to_float(value: Int) -> Float

@external(erlang, "math", "pow")
pub fn do_power(base: Float, exponent: Float) -> Float

@external(erlang, "rand", "uniform")
pub fn random() -> Float

@external(erlang, "math", "log")
pub fn do_log(value: Float) -> Float

@external(erlang, "math", "exp")
pub fn exponential(value: Float) -> Float
"#;

    fn execution(
        source: &str,
        modules: impl IntoIterator<Item = HostModule<GleamStdlibProfile>>,
    ) -> HostedExecution<GleamStdlibProfile> {
        let source = format!("{FLOAT_DECLARATIONS}\n{source}");
        let provider =
            host_provider::<GleamStdlibProfile>().expect("official float provider should register");
        let typed = compile_typed_host_program(
            "gleam_stdlib",
            "gleam/float",
            [PackageSource::new(
                "gleam_stdlib",
                Vec::<EcoString>::new(),
                [ModuleSource::new(
                    "gleam/float",
                    "src/gleam/float.gleam",
                    source,
                )],
            )],
            HostProviderSet::with_providers(modules, [provider])
                .expect("float provider module should be unique"),
        )
        .expect("synthetic float source should compile");
        let plan = plan_host_program(typed).expect("synthetic float source should plan");
        HostedExecution::try_from_module_plan(plan).expect("synthetic float execution should seal")
    }

    #[test]
    fn projects_only_the_stdlib_random_state() {
        let mut state = GleamStdlibRunState::from_seed([3; 32]);
        let projected =
            <FloatProvider<GleamStdlibProfile> as HostProvider<GleamStdlibProfile>>::project(
                &mut state,
            );

        assert!(std::ptr::eq(projected, &state));
    }

    #[test]
    fn implements_float_math_and_checked_conversions() {
        assert_eq!(to_string(2.0), "2.0");
        assert_eq!(ceiling(2.3), 3.0);
        assert_eq!(floor(2.7), 2.0);
        assert_eq!(js_round(2.5), Ok(BigInt::from(3)));
        assert_eq!(truncate(-2.9), Ok(BigInt::from(-2)));
        assert_eq!(do_to_float(BigInt::from(7)), Ok(7.0));
        assert_eq!(do_power(2.0, 3.0), 8.0);
        assert_eq!(do_log(std::f64::consts::E), 1.0);
        assert_eq!(exponential(1.0), std::f64::consts::E);
    }

    #[test]
    fn rejects_non_finite_or_unrepresentable_conversions() {
        assert_eq!(
            js_round(f64::NAN)
                .expect_err("NaN should not convert")
                .to_string(),
            "float cannot be represented as an Int",
        );
        assert_eq!(
            truncate(f64::INFINITY)
                .expect_err("infinity should not convert")
                .to_string(),
            "float cannot be represented as an Int",
        );
        assert_eq!(
            do_to_float(BigInt::from(10u8).pow(1000))
                .expect_err("an overflowing Int should not convert")
                .to_string(),
            "Int cannot be represented as a finite Float",
        );
    }

    #[test]
    fn executes_every_float_provider_with_reproducible_caller_state() {
        let source = r#"
pub fn main() {
  #(
    parse("2.5"),
    parse("2"),
    to_string(2.0),
    ceiling(2.3),
    floor(2.7),
    js_round(2.5),
    truncate(-2.9),
    do_to_float(7),
    do_power(2.0, 3.0),
    random(),
    do_log(1.0),
    exponential(0.0),
  )
}
"#;
        let execution = execution(source, Vec::<HostModule<GleamStdlibProfile>>::new());
        let mut expected_state = GleamStdlibRunState::from_seed([5; 32]);
        let expected_random = expected_state.random_float();
        let expected = format!(
            r#"#(Ok(2.5), Error(Nil), "2.0", 3.0, 2.0, 3, -2, 7.0, 8.0, {expected_random:?}, 0.0, 1.0)"#,
        );
        let mut first_state = GleamStdlibRunState::from_seed([5; 32]);
        let mut second_state = GleamStdlibRunState::from_seed([5; 32]);

        let first = execution
            .run_main(&mut first_state, &mut Vec::new())
            .expect("float providers should run");
        let second = execution
            .run_main(&mut second_state, &mut Vec::new())
            .expect("the same seed should reproduce float providers");
        let advanced = execution
            .run_main(&mut first_state, &mut Vec::new())
            .expect("reusing state should advance its random stream");

        assert_eq!(first.inspect().to_string(), expected);
        assert_eq!(second, first);
        assert_ne!(advanced, first);
    }

    #[test]
    fn reports_checked_conversion_failures_at_the_float_provider() {
        let cases = [
            (
                "import host/float_values\npub fn main() { js_round(float_values.nan()) }",
                "host function gleam_stdlib::gleam/float.js_round failed: float cannot be represented as an Int",
            ),
            (
                "import host/float_values\npub fn main() { truncate(float_values.infinity()) }",
                "host function gleam_stdlib::gleam/float.truncate failed: float cannot be represented as an Int",
            ),
            (
                "pub fn main() { do_to_float(10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000) }",
                "host function gleam_stdlib::gleam/float.do_to_float failed: Int cannot be represented as a finite Float",
            ),
        ];

        for (source, expected) in cases {
            let values = HostModule::<GleamStdlibProfile>::new_for_profile(
                "gleam_stdlib",
                "host/float_values",
            )
            .expect("float value module should be valid")
            .with_function("nan", || f64::NAN)
            .expect("NaN function should be valid")
            .with_function("infinity", || f64::INFINITY)
            .expect("infinity function should be valid");
            let execution = execution(source, [values]);
            let error = execution
                .run_main(
                    &mut GleamStdlibRunState::from_seed([0; 32]),
                    &mut Vec::new(),
                )
                .expect_err("checked conversion should fail");

            assert_eq!(error.to_string(), expected);
        }
    }
}