geam-stdlib 0.2.0

Official Gleam standard-library providers for Geam
Documentation
mod function;

use super::{Component, GleamStdlibHostProfile};
use crate::{BitArrayValue, HostProviderModule, HostRegistrationError};
use bitvec::order::Msb0;
use bitvec::vec::BitVec;
use ecow::EcoString;
use geam_core::provider::HostResult;
use geam_core::provider_support::{bit_array_bits, bit_array_from_bits};
use num_bigint::BigInt;

#[geam_macros::module(
    path = "gleam/bit_array",
    crate_path = geam_core,
    profile = crate::GleamStdlibHostProfile,
    component = crate::Component<Profile::Io>,
)]
mod provider {
    use super::{
        BigInt, BitArrayValue, BitVec, EcoString, HostResult, Msb0, bit_array_bits,
        bit_array_from_bits, function,
    };

    #[geam_macros::function]
    fn from_string(value: EcoString) -> BitArrayValue {
        function::from_string(value)
    }

    #[geam_macros::function]
    fn bit_size(value: BitArrayValue) -> BigInt {
        function::bit_size(value)
    }

    #[geam_macros::function]
    fn byte_size(value: BitArrayValue) -> BigInt {
        function::byte_size(value)
    }

    #[geam_macros::function]
    fn pad_to_bytes(value: BitArrayValue) -> BitArrayValue {
        function::pad_to_bytes(value)
    }

    #[geam_macros::function]
    fn slice(value: BitArrayValue, position: BigInt, length: BigInt) -> Result<BitArrayValue, ()> {
        function::slice(value, position, length)
    }

    #[geam_macros::function]
    fn unsafe_to_string(value: BitArrayValue) -> HostResult<EcoString> {
        function::unsafe_to_string(value).map_err(Into::into)
    }

    #[geam_macros::function]
    fn concat(values: geam_core::provider::List<BitArrayValue>) -> BitArrayValue {
        let mut bits = BitVec::<u8, Msb0>::new();
        let mut index = 0;
        while let Some(value) = values.get(index) {
            bits.extend_from_bitslice(bit_array_bits(&value));
            index += 1;
        }
        bit_array_from_bits(bits)
    }

    #[geam_macros::function]
    fn base64_encode(value: BitArrayValue, padding: bool) -> EcoString {
        function::base64_encode(value, padding)
    }

    #[geam_macros::function]
    fn decode64(value: EcoString) -> Result<BitArrayValue, ()> {
        function::decode64(value)
    }

    #[geam_macros::function]
    fn base16_encode(value: BitArrayValue) -> EcoString {
        function::base16_encode(value)
    }

    #[geam_macros::function]
    fn base16_decode(value: EcoString) -> Result<BitArrayValue, ()> {
        function::base16_decode(value)
    }

    #[geam_macros::function]
    fn bit_array_to_int_and_size(value: BitArrayValue) -> (BigInt, BigInt) {
        function::bit_array_to_int_and_size(value)
    }
}

pub(super) fn host_provider<Profile>() -> Result<HostProviderModule<Profile>, HostRegistrationError>
where
    Profile: GleamStdlibHostProfile,
{
    provider::__geam_module::<Profile>()
}

#[cfg(test)]
mod tests {
    use super::host_provider;
    use crate::{
        ExecutionError, 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};

    const BIT_ARRAY_DECLARATIONS: &str = r#"
@external(erlang, "host", "from_string")
fn from_string(value: String) -> BitArray

@external(erlang, "host", "bit_size")
fn bit_size(value: BitArray) -> Int

@external(erlang, "host", "byte_size")
fn byte_size(value: BitArray) -> Int

@external(erlang, "host", "pad_to_bytes")
fn pad_to_bytes(value: BitArray) -> BitArray

@external(erlang, "host", "slice")
fn slice(value: BitArray, position: Int, length: Int) -> Result(BitArray, Nil)

@external(erlang, "host", "unsafe_to_string")
fn unsafe_to_string(value: BitArray) -> String

@external(erlang, "host", "concat")
fn concat(values: List(BitArray)) -> BitArray

@external(erlang, "host", "base64_encode")
fn base64_encode(value: BitArray, padding: Bool) -> String

@external(erlang, "host", "decode64")
fn decode64(value: String) -> Result(BitArray, Nil)

@external(erlang, "host", "base16_encode")
fn base16_encode(value: BitArray) -> String

@external(erlang, "host", "base16_decode")
fn base16_decode(value: String) -> Result(BitArray, Nil)

@external(erlang, "host", "bit_array_to_int_and_size")
fn bit_array_to_int_and_size(value: BitArray) -> #(Int, Int)
"#;

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

    #[test]
    fn registers_the_exact_official_bit_array_provider_inventory() {
        let provider = host_provider::<GleamStdlibProfile>()
            .expect("official bit array provider should register");

        assert_eq!(provider.package(), "gleam_stdlib");
        assert_eq!(provider.module(), "gleam/bit_array");
        assert_eq!(provider.external_types().count(), 0);
        assert_eq!(
            provider
                .functions()
                .map(|function| function.name().as_str())
                .collect::<Vec<_>>(),
            [
                "from_string",
                "bit_size",
                "byte_size",
                "pad_to_bytes",
                "slice",
                "unsafe_to_string",
                "concat",
                "base64_encode",
                "decode64",
                "base16_encode",
                "base16_decode",
                "bit_array_to_int_and_size",
            ],
        );
    }

    #[test]
    fn executes_every_bit_array_provider_through_the_hosted_pipeline() {
        let execution = execution(
            r#"
pub fn main() {
  assert from_string("AB") == <<65, 66>>
  assert bit_size(<<5:size(3)>>) == 3
  assert byte_size(<<5:size(3)>>) == 1
  assert pad_to_bytes(<<5:size(3)>>) == <<5:size(3), 0:size(5)>>
  assert slice(<<1, 2, 3>>, 1, 1) == Ok(<<2>>)
  assert slice(<<1, 2, 3>>, 2, -1) == Ok(<<2>>)
  assert slice(<<1>>, 0, -1) == Error(Nil)
  assert slice(<<1, 2, 3>>, 4, 1) == Error(Nil)
  assert slice(<<1:size(2)>>, 0, 0) == Error(Nil)
  assert unsafe_to_string(<<65, 66>>) == "AB"
  assert concat([<<1>>, <<2:size(2)>>]) == <<1, 2:size(2)>>
  assert base64_encode(<<5:size(3)>>, True) == "oA=="
  assert base64_encode(<<5:size(3)>>, False) == "oA"
  assert decode64("AQ==") == Ok(<<1>>)
  assert decode64("AB==") == Ok(<<0>>)
  assert byte_size(from_string("aG  \t\nVsbG8=")) == 12
  assert decode64("aG  \t\nVsbG8=") == Ok(<<"hello":utf8>>)
  assert decode64("***=") == Error(Nil)
  assert base16_encode(<<5:size(3)>>) == "A0"
  assert base16_decode("01") == Ok(<<1>>)
  assert base16_decode("GG") == Error(Nil)
  bit_array_to_int_and_size(<<5:size(3)>>)
}
"#,
        );
        let value = execution
            .run_main(
                &mut GleamStdlibRunState::from_seed([0; 32]),
                &mut Vec::new(),
            )
            .expect("bit array providers should run");

        assert_eq!(value.inspect().to_string(), "#(5, 3)");
    }

    #[test]
    fn preserves_invalid_utf8_through_the_bit_array_host_adapter() {
        let execution = execution("pub fn main() { unsafe_to_string(<<255>>) }");
        let error = execution
            .run_main(
                &mut GleamStdlibRunState::from_seed([0; 32]),
                &mut Vec::new(),
            )
            .expect_err("invalid UTF-8 should fail");
        let error = expect_bit_array_host_error(error);

        assert_eq!(error.package(), "gleam_stdlib");
        assert_eq!(error.module(), "gleam/bit_array");
        assert_eq!(error.function(), "unsafe_to_string");
        assert_eq!(error.failure().message(), "bit array is not valid UTF-8");
    }

    #[test]
    #[should_panic(expected = "invalid UTF-8 should remain a host failure")]
    fn bit_array_host_failure_assertion_rejects_other_execution_errors() {
        let _ = expect_bit_array_host_error(ExecutionError::Invariant(
            InvariantError::ListIndexOutOfBounds {
                item_type: ValueType::BitArray,
                index: 1,
                length: 0,
            },
        ));
    }

    fn expect_bit_array_host_error(error: ExecutionError) -> Box<HostError> {
        let ExecutionError::Host(error) = error else {
            panic!("invalid UTF-8 should remain a host failure");
        };
        error
    }
}