geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
use super::DynamicSequence;
use super::schema::{Dynamic, DynamicList, DynamicSchema};
use super::storage::{DynamicExternalStorage, DynamicPayload, DynamicRepresentation};
use crate::gleam_stdlib::GleamStdlibHostProfile;
use crate::{
    HostCall, HostCallCompletion, HostCallError, HostConstruction, HostExternal,
    HostExternalBinding, HostList, HostProvider, HostType,
};
use ecow::EcoString;
use std::marker::PhantomData;

pub(in crate::gleam_stdlib) struct DynamicProvider<Profile>(PhantomData<Profile>);

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

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

impl<Profile> HostExternalBinding<Profile, DynamicSchema> for DynamicProvider<Profile>
where
    Profile: GleamStdlibHostProfile,
{
    type Storage = DynamicExternalStorage;
}

pub(super) fn classify<'call, Profile>(
    call: HostCall<'call, Profile, DynamicProvider<Profile>, EcoString>,
    value: HostExternal<'call, Dynamic>,
) -> Result<HostCallCompletion<'call, EcoString>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let name = call.external_payload(value).representation().name().into();
    Ok(call.return_value(name))
}

pub(super) fn cast<'call, Profile, Type>(
    mut call: HostCall<'call, Profile, DynamicProvider<Profile>, Dynamic>,
    value: Type::Value<'call>,
) -> Result<HostCallCompletion<'call, Dynamic>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
    Type: HostType,
{
    let dynamic = create_return_value::<Profile, DynamicProvider<Profile>, Type>(&mut call, value);
    Ok(call.return_value(dynamic))
}

pub(super) fn array<'call, Profile>(
    mut call: HostCall<'call, Profile, DynamicProvider<Profile>, Dynamic>,
    values: HostList<'call, Dynamic>,
) -> Result<HostCallCompletion<'call, Dynamic>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let mut index = 0;
    let mut sequence = Vec::new();
    while let Some(value) = call.list_item(values, index) {
        sequence.push(value);
        index += 1;
    }
    let dynamic = call.create_external_with(move |builder| DynamicPayload::Array {
        value: builder.store_dynamic::<DynamicList>(values),
        elements: sequence
            .into_iter()
            .map(|value| builder.store_dynamic::<Dynamic>(value))
            .collect(),
    });
    Ok(call.return_value(dynamic))
}

pub(in crate::gleam_stdlib) fn classification<'call, Profile, Provider, Return>(
    call: &HostCall<'call, Profile, Provider, Return>,
    value: HostExternal<'call, Dynamic>,
) -> EcoString
where
    Profile: GleamStdlibHostProfile,
    Provider: HostExternalBinding<Profile, DynamicSchema, Storage = DynamicExternalStorage>,
    Return: HostType,
{
    call.external_payload(value).representation().name().into()
}

pub(crate) fn create_return_value<'call, Profile, Provider, Type>(
    call: &mut HostCall<'call, Profile, Provider, Dynamic>,
    value: Type::Value<'call>,
) -> HostExternal<'call, Dynamic>
where
    Profile: GleamStdlibHostProfile,
    Provider: HostExternalBinding<Profile, DynamicSchema, Storage = DynamicExternalStorage>,
    Type: HostType,
{
    call.create_external_with(|builder| {
        let value = builder.store_dynamic::<Type>(value);
        DynamicPayload::Stored {
            representation: DynamicRepresentation::from_value(&value),
            value,
        }
    })
}

pub(crate) fn create_value<'call, Profile, Provider, Return, Type>(
    call: &mut HostCall<'call, Profile, Provider, Return>,
    construction: HostConstruction<'call, Dynamic>,
    value: Type::Value<'call>,
) -> HostExternal<'call, Dynamic>
where
    Profile: GleamStdlibHostProfile,
    Provider: HostExternalBinding<Profile, DynamicSchema, Storage = DynamicExternalStorage>,
    Return: HostType,
    Type: HostType,
{
    call.construct_external_with::<DynamicSchema, crate::HostTypeListEnd>(construction, |builder| {
        let value = builder.store_dynamic::<Type>(value);
        DynamicPayload::Stored {
            representation: DynamicRepresentation::from_value(&value),
            value,
        }
    })
}

pub(in crate::gleam_stdlib) fn decode_value<'call, Profile, Provider, Return, Type>(
    call: &mut HostCall<'call, Profile, Provider, Return>,
    value: HostExternal<'call, Dynamic>,
) -> Option<Type::Value<'call>>
where
    Profile: GleamStdlibHostProfile,
    Provider: HostExternalBinding<Profile, DynamicSchema, Storage = DynamicExternalStorage>,
    Return: HostType,
    Type: HostType,
{
    let payload = call.external_payload(value);
    payload.decode::<Profile, Provider, Return, Type>(call, DynamicPayload::value)
}

pub(in crate::gleam_stdlib) fn sequence<'call, Profile, Provider, Return>(
    call: &mut HostCall<'call, Profile, Provider, Return>,
    value: HostExternal<'call, Dynamic>,
) -> Option<DynamicSequence<'call>>
where
    Profile: GleamStdlibHostProfile,
    Provider: HostExternalBinding<Profile, DynamicSchema, Storage = DynamicExternalStorage>,
    Return: HostType,
{
    let payload = call.external_payload(value);
    let sequence = match payload.representation() {
        DynamicRepresentation::List => DynamicSequence::List,
        DynamicRepresentation::Array => DynamicSequence::Array,
        _ => return None,
    };
    payload
        .decode::<Profile, Provider, Return, DynamicList>(call, DynamicPayload::value)
        .map(sequence)
}

#[cfg(test)]
mod tests {
    use super::super::host_provider;
    use super::{Dynamic, DynamicProvider};
    use crate::gleam_stdlib::{GleamStdlibProfile, GleamStdlibRunState};
    use crate::{
        HostCall, HostCallCompletion, HostCallError, HostExternal, HostModule, HostProvider,
        HostProviderSet, HostedExecution, ModuleSource, PackageSource, Value,
        compile_typed_host_program, plan_host_program,
    };
    use ecow::EcoString;
    use num_bigint::BigInt;

    struct HashProvider;

    impl HostProvider<GleamStdlibProfile> for HashProvider {
        type State = GleamStdlibRunState;

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

    fn source_hash<'call>(
        call: HostCall<'call, GleamStdlibProfile, HashProvider, BigInt>,
        value: HostExternal<'call, Dynamic>,
    ) -> Result<HostCallCompletion<'call, BigInt>, HostCallError> {
        let hash = BigInt::from(call.source_hash::<Dynamic>(value));
        Ok(call.return_value(hash))
    }

    const DYNAMIC_DECLARATIONS: &str = r#"
@external(erlang, "host", "Dynamic")
pub type Dynamic

@external(erlang, "host", "classify")
pub fn classify(value: Dynamic) -> String

@external(erlang, "host", "bool")
pub fn bool(value: Bool) -> Dynamic

@external(erlang, "host", "string")
pub fn string(value: String) -> Dynamic

@external(erlang, "host", "float")
pub fn float(value: Float) -> Dynamic

@external(erlang, "host", "int")
pub fn int(value: Int) -> Dynamic

@external(erlang, "host", "bit_array")
pub fn bit_array(value: BitArray) -> Dynamic

@external(erlang, "host", "list")
pub fn list(value: List(Dynamic)) -> Dynamic

@external(erlang, "host", "array")
pub fn array(value: List(Dynamic)) -> Dynamic

@external(erlang, "host", "cast")
pub fn cast(value: value) -> Dynamic
"#;

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

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

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

        let projected = <HashProvider as HostProvider<GleamStdlibProfile>>::project(&mut state);

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

    #[test]
    fn generic_cast_classifies_recursive_and_callable_families() {
        let source = r#"
pub type Boxed {
  Boxed(Int)
}

fn increment(value: Int) {
  value + 1
}

pub fn main() {
  let assert <<codepoint:utf8_codepoint>> = <<65>>
  #(
    classify(cast(codepoint)),
    classify(cast(#(1, "one"))),
    classify(cast([1, 2])),
    classify(cast(increment)),
    classify(cast(Boxed(1))),
    classify(cast(Nil)),
  )
}
"#;
        let actual = execution(source, Vec::<HostModule<GleamStdlibProfile>>::new())
            .run_main(
                &mut GleamStdlibRunState::from_seed([0; 32]),
                &mut Vec::new(),
            )
            .expect("generic dynamic casts should run");

        assert_eq!(
            actual,
            Value::Tuple(vec![
                Value::String("UtfCodepoint".into()),
                Value::String("Array".into()),
                Value::String("List".into()),
                Value::String("Function".into()),
                Value::String("Custom".into()),
                Value::String("Nil".into()),
            ]),
        );
    }

    #[test]
    fn public_constructors_preserve_representation_equality_hash_and_inspection() {
        let hash =
            HostModule::<GleamStdlibProfile>::new_for_profile("gleam_stdlib", "host/dynamic_hash")
                .expect("hash module should be valid")
                .with_scoped_function::<HashProvider, (Dynamic,), BigInt, _>(
                    "source_hash",
                    source_hash,
                )
                .expect("hash function should be valid");
        let source = r#"
import host/dynamic_hash

pub fn main() {
  let bool_value = bool(True)
  let string_value = string("one")
  let float_value = float(1.5)
  let first_int = int(42)
  let second_int = int(42)
  let second_string = string("one")
  let bits = bit_array(<<1, 2>>)
  let list_value = list([first_int, string_value])
  let array_value = array([first_int, string_value])
  let second_array = array([second_int, second_string])
  let empty_array = array([])

  assert classify(bool_value) == "Bool"
  assert classify(string_value) == "String"
  assert classify(float_value) == "Float"
  assert classify(first_int) == "Int"
  assert classify(bits) == "BitArray"
  assert classify(list_value) == "List"
  assert classify(array_value) == "Array"
  assert first_int == second_int
  assert list_value != array_value
  assert array_value == second_array
  assert dynamic_hash.source_hash(first_int)
    == dynamic_hash.source_hash(second_int)
  assert dynamic_hash.source_hash(array_value)
    == dynamic_hash.source_hash(second_array)

  #(
    bool_value,
    string_value,
    float_value,
    first_int,
    bits,
    list_value,
    array_value,
    empty_array,
  )
}
"#;
        let actual = execution(source, [hash])
            .run_main(
                &mut GleamStdlibRunState::from_seed([0; 32]),
                &mut Vec::new(),
            )
            .expect("public dynamic constructors should run");

        assert_eq!(
            actual.inspect().to_string(),
            r#"#(True, "one", 1.5, 42, <<1, 2>>, [42, "one"], #(42, "one"), #())"#,
        );
    }
}