geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
mod codec;

use super::schema::{
    CodeunitPair, PercentDecodeError, PercentDecodeOk, PercentDecodeResult, QueryConstructions,
    QueryError, QueryOk, QueryPairIndex, QueryPairsIndex, QueryResult,
};
use crate::gleam_stdlib::GleamStdlibHostProfile;
use crate::{
    HostCall, HostCallCompletion, HostCallError, HostConstructions, HostFailure, HostProvider,
};
use ecow::EcoString;
use num_bigint::BigInt;
use num_traits::ToPrimitive;
use std::marker::PhantomData;

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

impl<Profile> HostProvider<Profile> for UriProvider<Profile>
where
    Profile: GleamStdlibHostProfile,
{
    type State = Profile::RunState;

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

pub(super) fn pop_codeunit<'call, Profile>(
    call: HostCall<'call, Profile, UriProvider<Profile>, CodeunitPair>,
    string: EcoString,
) -> Result<HostCallCompletion<'call, CodeunitPair>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let Some(value) = string.chars().next() else {
        return Ok(call.return_tuple((BigInt::from(0), (string, ()))));
    };
    let rest = EcoString::from(&string[value.len_utf8()..]);

    Ok(call.return_tuple((BigInt::from(u32::from(value)), (rest, ()))))
}

pub(super) fn codeunit_slice(
    string: EcoString,
    from: BigInt,
    length: BigInt,
) -> Result<EcoString, HostFailure> {
    let from = from
        .to_usize()
        .ok_or_else(|| HostFailure::new("URI string slice index is not representable"))?;
    let length = length
        .to_usize()
        .ok_or_else(|| HostFailure::new("URI string slice length is not representable"))?;
    let end = from
        .checked_add(length)
        .ok_or_else(|| HostFailure::new("URI string slice range is not representable"))?;
    let from = scalar_byte_index(&string, from)
        .ok_or_else(|| HostFailure::new("URI string slice starts outside the string"))?;
    let end = scalar_byte_index(&string, end)
        .ok_or_else(|| HostFailure::new("URI string slice ends outside the string"))?;

    Ok(EcoString::from(&string[from..end]))
}

pub(super) fn parse_query<'call, Profile>(
    mut call: HostCall<'call, Profile, UriProvider<Profile>, QueryResult>,
    constructions: HostConstructions<'call, QueryConstructions>,
    query: EcoString,
) -> Result<HostCallCompletion<'call, QueryResult>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let Some(pairs) = codec::parse_query(&query) else {
        return Ok(call.return_custom::<QueryError>(((), ())));
    };
    let pairs = pairs
        .into_iter()
        .map(|(key, value)| {
            call.construct_tuple(constructions.at::<QueryPairIndex>(), (key, (value, ())))
        })
        .collect::<Vec<_>>();
    let pairs = call.construct_list(constructions.at::<QueryPairsIndex>(), pairs);

    Ok(call.return_custom::<QueryOk>((pairs, ())))
}

pub(super) fn percent_encode(value: EcoString) -> EcoString {
    codec::percent_encode(&value)
}

pub(super) fn percent_decode<'call, Profile>(
    call: HostCall<'call, Profile, UriProvider<Profile>, PercentDecodeResult>,
    value: EcoString,
) -> Result<HostCallCompletion<'call, PercentDecodeResult>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    Ok(match codec::percent_decode(&value) {
        Some(value) => call.return_custom::<PercentDecodeOk>((value, ())),
        None => call.return_custom::<PercentDecodeError>(((), ())),
    })
}

fn scalar_byte_index(string: &str, index: usize) -> Option<usize> {
    if index == string.chars().count() {
        return Some(string.len());
    }

    string.char_indices().nth(index).map(|(index, _)| index)
}

#[cfg(test)]
mod tests {
    use super::{UriProvider, codeunit_slice, scalar_byte_index};
    use crate::gleam_stdlib::{GleamStdlibProfile, GleamStdlibRunState};
    use crate::{
        HostModule, HostProvider, HostProviderSet, HostedExecution, ModuleSource, PackageSource,
        Value, compile_typed_host_program, plan_host_program,
    };
    use ecow::EcoString;
    use num_bigint::BigInt;

    const URI_SOURCE: &str = r#"
@external(erlang, "gleam_stdlib", "string_pop_codeunit")
fn pop_codeunit(string: String) -> #(Int, String)

@external(erlang, "binary", "part")
fn codeunit_slice(string: String, from: Int, length: Int) -> String

@external(erlang, "gleam_stdlib", "parse_query")
pub fn parse_query(query: String) -> Result(List(#(String, String)), Nil)

@external(erlang, "gleam_stdlib", "percent_encode")
pub fn percent_encode(value: String) -> String

@external(erlang, "gleam_stdlib", "percent_decode")
pub fn percent_decode(value: String) -> Result(String, Nil)

pub fn main() {
  let #(codepoint, rest) = pop_codeunit("ñrest")
  assert codepoint == 241
  assert rest == "rest"

  let #(empty_codepoint, empty_rest) = pop_codeunit("")
  assert empty_codepoint == 0
  assert empty_rest == ""
  assert codeunit_slice("año", 1, 2) == "ño"
  assert parse_query("a+b=1&a+b=2") == Ok([#("a b", "1"), #("a b", "2")])
  assert parse_query("%C2") == Error(Nil)
  assert percent_encode("ñ +") == "%C3%B1%20+"
  assert percent_decode("%C3%B1%20+") == Ok("ñ +")
  assert percent_decode("%C2") == Error(Nil)
}
"#;

    #[test]
    fn uri_provider_projects_the_caller_owned_run_state() {
        let mut state = GleamStdlibRunState::from_seed([3; 32]);
        let expected = std::ptr::from_mut(&mut state);
        let projected =
            <UriProvider<GleamStdlibProfile> as HostProvider<GleamStdlibProfile>>::project(
                &mut state,
            );

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

    #[test]
    fn slices_uri_strings_by_unicode_scalar_index() {
        assert_eq!(
            codeunit_slice("año".into(), 1.into(), 1.into()),
            Ok("ñ".into())
        );
        assert_eq!(
            codeunit_slice("año".into(), 1.into(), 2.into()),
            Ok("ño".into())
        );
        assert_eq!(
            codeunit_slice("año".into(), 3.into(), 0.into()),
            Ok("".into())
        );
    }

    #[test]
    fn rejects_unrepresentable_or_out_of_bounds_uri_slices() {
        for (from, length, message) in [
            (
                BigInt::from(-1),
                BigInt::from(1),
                "URI string slice index is not representable",
            ),
            (
                BigInt::from(0),
                BigInt::from(-1),
                "URI string slice length is not representable",
            ),
            (
                BigInt::from(usize::MAX),
                BigInt::from(1),
                "URI string slice range is not representable",
            ),
            (
                BigInt::from(4),
                BigInt::from(0),
                "URI string slice starts outside the string",
            ),
            (
                BigInt::from(2),
                BigInt::from(2),
                "URI string slice ends outside the string",
            ),
        ] {
            assert_eq!(
                codeunit_slice("año".into(), from, length)
                    .expect_err("invalid URI string slice should fail")
                    .message(),
                message,
            );
        }

        assert_eq!(scalar_byte_index("año", usize::MAX), None);
    }

    #[test]
    fn executes_every_uri_provider_through_the_typed_hosted_pipeline() {
        let provider = super::super::host_provider::<GleamStdlibProfile>()
            .expect("synthetic URI provider should register");
        let typed = compile_typed_host_program(
            "gleam_stdlib",
            "gleam/uri",
            [PackageSource::new(
                "gleam_stdlib",
                Vec::<EcoString>::new(),
                [ModuleSource::new(
                    "gleam/uri",
                    "src/gleam/uri.gleam",
                    URI_SOURCE,
                )],
            )],
            HostProviderSet::with_providers(
                Vec::<HostModule<GleamStdlibProfile>>::new(),
                [provider],
            )
            .expect("synthetic URI provider module should be unique"),
        )
        .expect("synthetic URI source should compile");
        let plan = plan_host_program(typed).expect("synthetic URI source should plan");
        let execution =
            HostedExecution::try_from_module_plan(plan).expect("synthetic URI source should seal");

        assert_eq!(
            execution
                .run_main(
                    &mut GleamStdlibRunState::from_seed([5; 32]),
                    &mut Vec::new(),
                )
                .expect("synthetic URI source should run"),
            Value::Nil,
        );
    }
}