geam-stdlib 0.2.0

Official Gleam standard-library providers for Geam
Documentation
use super::parse::{decimal, format_radix, radix};
use crate::HostFailure;
use ecow::EcoString;
use num_bigint::{BigInt, Sign};
use num_traits::ToPrimitive;
pub(super) fn parse(source: EcoString) -> Result<BigInt, ()> {
    decimal(&source).ok_or(())
}

pub(super) fn do_base_parse(source: EcoString, base: BigInt) -> Result<BigInt, ()> {
    radix(&source, &base).ok_or(())
}

pub(super) fn to_string(value: BigInt) -> EcoString {
    value.to_string().into()
}

pub(super) fn do_to_base_string(value: BigInt, base: BigInt) -> Result<EcoString, HostFailure> {
    format_radix(&value, &base)
        .ok_or_else(|| HostFailure::new("base must be an Int from 2 through 36"))
}

pub(super) fn to_float(value: BigInt) -> Result<f64, HostFailure> {
    super::super::float::do_to_float(value)
}

pub(super) fn bitwise_and(left: BigInt, right: BigInt) -> BigInt {
    left & right
}

pub(super) fn bitwise_not(value: BigInt) -> BigInt {
    !value
}

pub(super) fn bitwise_or(left: BigInt, right: BigInt) -> BigInt {
    left | right
}

pub(super) fn bitwise_exclusive_or(left: BigInt, right: BigInt) -> BigInt {
    left ^ right
}

pub(super) fn bitwise_shift_left(value: BigInt, shift: BigInt) -> Result<BigInt, HostFailure> {
    let reversed = shift.sign() == Sign::Minus;
    let count = shift
        .magnitude()
        .to_usize()
        .ok_or_else(|| HostFailure::new("bit shift count cannot be represented by this host"))?;
    Ok(if reversed {
        value >> count
    } else {
        value << count
    })
}

pub(super) fn bitwise_shift_right(value: BigInt, shift: BigInt) -> Result<BigInt, HostFailure> {
    let reversed = shift.sign() == Sign::Minus;
    let count = shift
        .magnitude()
        .to_usize()
        .ok_or_else(|| HostFailure::new("bit shift count cannot be represented by this host"))?;
    Ok(if reversed {
        value << count
    } else {
        value >> count
    })
}

#[cfg(test)]
mod tests {
    use super::super::host_provider;
    use super::{
        bitwise_and, bitwise_exclusive_or, bitwise_not, bitwise_or, bitwise_shift_left,
        bitwise_shift_right, do_to_base_string, to_float, to_string,
    };
    use crate::{
        ExecutionError, HostFailure, HostModule, HostProviderSet, HostedExecution, ModuleSource,
        PackageSource, ValueType, compile_typed_host_program, plan_host_program,
    };
    use crate::{GleamStdlibProfile, GleamStdlibRunState};
    use ecow::EcoString;
    use geam_core::{HostError, InvariantError};
    use num_bigint::BigInt;

    const INT_DECLARATIONS: &str = r#"
@external(erlang, "gleam_stdlib", "parse_int")
pub fn parse(value: String) -> Result(Int, Nil)

@external(erlang, "gleam_stdlib", "int_from_base_string")
pub fn do_base_parse(value: String, base: Int) -> Result(Int, Nil)

@external(erlang, "erlang", "integer_to_binary")
pub fn to_string(value: Int) -> String

@external(erlang, "erlang", "integer_to_binary")
pub fn do_to_base_string(value: Int, base: Int) -> String

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

@external(erlang, "erlang", "band")
pub fn bitwise_and(left: Int, right: Int) -> Int

@external(erlang, "erlang", "bnot")
pub fn bitwise_not(value: Int) -> Int

@external(erlang, "erlang", "bor")
pub fn bitwise_or(left: Int, right: Int) -> Int

@external(erlang, "erlang", "bxor")
pub fn bitwise_exclusive_or(left: Int, right: Int) -> Int

@external(erlang, "erlang", "bsl")
pub fn bitwise_shift_left(value: Int, shift: Int) -> Int

@external(erlang, "erlang", "bsr")
pub fn bitwise_shift_right(value: Int, shift: Int) -> Int
"#;

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

    #[test]
    fn implements_integer_formatting_conversion_and_bitwise_operations() {
        let large = BigInt::from(10u8).pow(100);

        assert_eq!(to_string(large.clone()), large.to_string());
        assert_eq!(do_to_base_string(255.into(), 16.into()), Ok("FF".into()));
        assert_eq!(to_float(7.into()), Ok(7.0));
        assert_eq!(bitwise_and(5.into(), 3.into()), BigInt::from(1));
        assert_eq!(bitwise_not(5.into()), BigInt::from(-6));
        assert_eq!(bitwise_or(5.into(), 2.into()), BigInt::from(7));
        assert_eq!(bitwise_exclusive_or(5.into(), 3.into()), BigInt::from(6));
        assert_eq!(bitwise_shift_left(1.into(), 5.into()), Ok(32.into()));
        assert_eq!(bitwise_shift_left(8.into(), (-1).into()), Ok(4.into()));
        assert_eq!(bitwise_shift_right(32.into(), 2.into()), Ok(8.into()));
        assert_eq!(bitwise_shift_right(8.into(), (-1).into()), Ok(16.into()));
    }

    #[test]
    fn executes_every_integer_provider_through_the_hosted_pipeline() {
        let execution = execution(
            r#"
pub fn main() {
  #(
    parse("12"),
    parse("bad"),
    do_base_parse("-FF", 16),
    do_base_parse("2", 2),
    to_string(123),
    do_to_base_string(255, 16),
    to_float(7),
    bitwise_and(5, 3),
    bitwise_not(5),
    bitwise_or(5, 2),
    bitwise_exclusive_or(5, 3),
    bitwise_shift_left(1, 5),
    bitwise_shift_right(32, 2),
  )
}
"#,
        );

        let value = execution
            .run_main(
                &mut GleamStdlibRunState::from_seed([0; 32]),
                &mut Vec::new(),
            )
            .expect("integer providers should run");

        assert_eq!(
            value.inspect().to_string(),
            r#"#(Ok(12), Error(Nil), Ok(-255), Error(Nil), "123", "FF", 7.0, 1, -6, 7, 6, 32, 8)"#,
        );
    }

    #[test]
    fn rejects_unrepresentable_conversion_bases_and_shift_counts() {
        let huge = BigInt::from(10u8).pow(1000);

        assert_eq!(
            to_float(huge.clone()),
            Err(HostFailure::new(
                "Int cannot be represented as a finite Float"
            )),
        );
        assert_eq!(
            do_to_base_string(1.into(), huge.clone()),
            Err(HostFailure::new("base must be an Int from 2 through 36")),
        );
        assert_eq!(
            bitwise_shift_left(1.into(), huge.clone()),
            Err(HostFailure::new(
                "bit shift count cannot be represented by this host"
            )),
        );
        assert_eq!(
            bitwise_shift_right(1.into(), -huge),
            Err(HostFailure::new(
                "bit shift count cannot be represented by this host"
            )),
        );
    }

    #[test]
    fn preserves_checked_integer_failures_through_the_host_adapter() {
        let huge = "1".repeat(400);
        let cases = [
            (
                "do_to_base_string(1, 1)".to_owned(),
                "do_to_base_string",
                "base must be an Int from 2 through 36",
            ),
            (
                format!("to_float({huge})"),
                "to_float",
                "Int cannot be represented as a finite Float",
            ),
            (
                format!("bitwise_shift_left(1, {huge})"),
                "bitwise_shift_left",
                "bit shift count cannot be represented by this host",
            ),
            (
                format!("bitwise_shift_right(1, -{huge})"),
                "bitwise_shift_right",
                "bit shift count cannot be represented by this host",
            ),
        ];

        for (call, function, message) in cases {
            let execution = execution(&format!("pub fn main() {{ {call} }}"));
            let error = execution
                .run_main(
                    &mut GleamStdlibRunState::from_seed([0; 32]),
                    &mut Vec::new(),
                )
                .expect_err("checked integer conversion should fail");
            let error = expect_integer_host_error(error);

            assert_eq!(error.package(), "gleam_stdlib");
            assert_eq!(error.module(), "gleam/int");
            assert_eq!(error.function(), function);
            assert_eq!(error.failure().message(), message);
        }
    }

    #[test]
    #[should_panic(expected = "checked integer conversion should remain a host failure")]
    fn integer_host_failure_assertion_rejects_other_execution_errors() {
        let _ = expect_integer_host_error(ExecutionError::Invariant(
            InvariantError::ListIndexOutOfBounds {
                item_type: ValueType::Int,
                index: 1,
                length: 0,
            },
        ));
    }

    fn expect_integer_host_error(error: ExecutionError) -> Box<HostError> {
        let ExecutionError::Host(error) = error else {
            panic!("checked integer conversion should remain a host failure");
        };
        error
    }
}