geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
use super::schema::{
    Direction, SplitConstructions, SplitStringTreeIndex, StringList, StringTree, StringTreeList,
};
use super::storage::{
    StringTree as StoredStringTree, StringTreeExternalStorage, StringTreePayload,
};
use crate::gleam_stdlib::GleamStdlibHostProfile;
use crate::{
    HostCall, HostCallCompletion, HostCallError, HostConstructions, HostCustom, HostExternal,
    HostExternalBinding, HostList, HostProvider,
};
use ecow::EcoString;
use num_bigint::BigInt;
use std::marker::PhantomData;
use unicode_segmentation::UnicodeSegmentation;

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

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

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

impl<Profile> HostExternalBinding<Profile, super::schema::StringTreeSchema>
    for StringTreeProvider<Profile>
where
    Profile: GleamStdlibHostProfile,
{
    type Storage = StringTreeExternalStorage;
}

pub(super) fn append_tree<'call, Profile>(
    mut call: HostCall<'call, Profile, StringTreeProvider<Profile>, StringTree>,
    tree: HostExternal<'call, StringTree>,
    suffix: HostExternal<'call, StringTree>,
) -> Result<HostCallCompletion<'call, StringTree>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let tree = call.external_payload(tree).tree.clone();
    let suffix = call.external_payload(suffix).tree.clone();
    let value = call.create_external(StringTreePayload {
        tree: tree.append(&suffix),
    });
    Ok(call.return_value(value))
}

pub(super) fn from_strings<'call, Profile>(
    mut call: HostCall<'call, Profile, StringTreeProvider<Profile>, StringTree>,
    strings: HostList<'call, EcoString>,
) -> Result<HostCallCompletion<'call, StringTree>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let mut index = 0;
    let mut trees = Vec::with_capacity(call.list_len(strings));
    while let Some(string) = call.list_item(strings, index) {
        trees.push(StoredStringTree::text(string));
        index += 1;
    }
    let value = call.create_external(StringTreePayload {
        tree: StoredStringTree::sequence(trees),
    });
    Ok(call.return_value(value))
}

pub(super) fn concat<'call, Profile>(
    mut call: HostCall<'call, Profile, StringTreeProvider<Profile>, StringTree>,
    values: HostList<'call, StringTree>,
) -> Result<HostCallCompletion<'call, StringTree>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let mut index = 0;
    let mut trees = Vec::with_capacity(call.list_len(values));
    while let Some(value) = call.list_item(values, index) {
        trees.push(call.external_payload(value).tree.clone());
        index += 1;
    }
    let value = call.create_external(StringTreePayload {
        tree: StoredStringTree::sequence(trees),
    });
    Ok(call.return_value(value))
}

pub(super) fn from_string<'call, Profile>(
    mut call: HostCall<'call, Profile, StringTreeProvider<Profile>, StringTree>,
    string: EcoString,
) -> Result<HostCallCompletion<'call, StringTree>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let value = call.create_external(StringTreePayload {
        tree: StoredStringTree::text(string),
    });
    Ok(call.return_value(value))
}

pub(super) fn to_string<'call, Profile>(
    call: HostCall<'call, Profile, StringTreeProvider<Profile>, EcoString>,
    tree: HostExternal<'call, StringTree>,
) -> Result<HostCallCompletion<'call, EcoString>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let string = call.external_payload(tree).tree.flatten();
    Ok(call.return_value(string))
}

pub(super) fn byte_size<'call, Profile>(
    call: HostCall<'call, Profile, StringTreeProvider<Profile>, BigInt>,
    tree: HostExternal<'call, StringTree>,
) -> Result<HostCallCompletion<'call, BigInt>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let size = BigInt::from(call.external_payload(tree).tree.byte_len());
    Ok(call.return_value(size))
}

pub(super) fn lowercase<'call, Profile>(
    mut call: HostCall<'call, Profile, StringTreeProvider<Profile>, StringTree>,
    tree: HostExternal<'call, StringTree>,
) -> Result<HostCallCompletion<'call, StringTree>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let value = call.external_payload(tree).tree.flatten().to_lowercase();
    let value = call.create_external(StringTreePayload {
        tree: StoredStringTree::text(value),
    });
    Ok(call.return_value(value))
}

pub(super) fn uppercase<'call, Profile>(
    mut call: HostCall<'call, Profile, StringTreeProvider<Profile>, StringTree>,
    tree: HostExternal<'call, StringTree>,
) -> Result<HostCallCompletion<'call, StringTree>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let value = call.external_payload(tree).tree.flatten().to_uppercase();
    let value = call.create_external(StringTreePayload {
        tree: StoredStringTree::text(value),
    });
    Ok(call.return_value(value))
}

pub(super) fn do_to_graphemes<'call, Profile>(
    call: HostCall<'call, Profile, StringTreeProvider<Profile>, StringList>,
    string: EcoString,
) -> Result<HostCallCompletion<'call, StringList>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    Ok(call.return_list(
        string
            .graphemes(true)
            .map(EcoString::from)
            .collect::<Vec<_>>(),
    ))
}

pub(super) fn erl_split<'call, Profile>(
    mut call: HostCall<'call, Profile, StringTreeProvider<Profile>, StringTreeList>,
    constructions: HostConstructions<'call, SplitConstructions>,
    tree: HostExternal<'call, StringTree>,
    pattern: EcoString,
    _direction: HostCustom<'call, Direction>,
) -> Result<HostCallCompletion<'call, StringTreeList>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let text = call.external_payload(tree).tree.flatten();
    let parts = if pattern.is_empty() {
        vec![text]
    } else {
        text.split(pattern.as_str()).map(EcoString::from).collect()
    };
    let mut values = Vec::with_capacity(parts.len());
    for part in parts {
        values.push(call.construct_external(
            constructions.at::<SplitStringTreeIndex>(),
            StringTreePayload {
                tree: StoredStringTree::text(part),
            },
        ));
    }
    Ok(call.return_list(values))
}

pub(super) fn replace<'call, Profile>(
    mut call: HostCall<'call, Profile, StringTreeProvider<Profile>, StringTree>,
    tree: HostExternal<'call, StringTree>,
    pattern: EcoString,
    substitute: EcoString,
) -> Result<HostCallCompletion<'call, StringTree>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let text = call.external_payload(tree).tree.flatten();
    let replaced = if pattern.is_empty() {
        text
    } else {
        text.replace(pattern.as_str(), substitute.as_str())
    };
    let value = call.create_external(StringTreePayload {
        tree: StoredStringTree::text(replaced),
    });
    Ok(call.return_value(value))
}

pub(super) fn is_equal<'call, Profile>(
    call: HostCall<'call, Profile, StringTreeProvider<Profile>, bool>,
    left: HostExternal<'call, StringTree>,
    right: HostExternal<'call, StringTree>,
) -> Result<HostCallCompletion<'call, bool>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let left = call.external_payload(left).tree.flatten();
    let right = call.external_payload(right).tree.flatten();
    Ok(call.return_value(left == right))
}

pub(super) fn is_empty<'call, Profile>(
    call: HostCall<'call, Profile, StringTreeProvider<Profile>, bool>,
    tree: HostExternal<'call, StringTree>,
) -> Result<HostCallCompletion<'call, bool>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    let empty = call.external_payload(tree).tree.byte_len() == 0;
    Ok(call.return_value(empty))
}

#[cfg(test)]
mod tests {
    use super::super::host_provider;
    use super::StringTreeProvider;
    use crate::gleam_stdlib::{GleamStdlibProfile, GleamStdlibRunState};
    use crate::{
        HostModule, HostProvider, HostProviderSet, HostedExecution, ModuleSource, PackageSource,
        compile_typed_host_program, plan_host_program,
    };
    use ecow::EcoString;

    const STRING_TREE_DECLARATIONS: &str = r#"
pub type StringTree

type Direction {
  All
}

@external(erlang, "host", "append_tree")
pub fn append_tree(tree: StringTree, suffix: StringTree) -> StringTree

@external(erlang, "host", "from_strings")
pub fn from_strings(strings: List(String)) -> StringTree

@external(erlang, "host", "concat")
pub fn concat(trees: List(StringTree)) -> StringTree

@external(erlang, "host", "from_string")
pub fn from_string(string: String) -> StringTree

@external(erlang, "host", "to_string")
pub fn to_string(tree: StringTree) -> String

@external(erlang, "host", "byte_size")
pub fn byte_size(tree: StringTree) -> Int

@external(erlang, "host", "lowercase")
pub fn lowercase(tree: StringTree) -> StringTree

@external(erlang, "host", "uppercase")
pub fn uppercase(tree: StringTree) -> StringTree

@external(erlang, "host", "do_to_graphemes")
@external(javascript, "host", "do_to_graphemes")
fn do_to_graphemes(string: String) -> List(String)

@external(erlang, "host", "erl_split")
fn erl_split(tree: StringTree, pattern: String, direction: Direction) -> List(StringTree)

@external(erlang, "host", "replace")
pub fn replace(tree: StringTree, pattern: String, substitute: String) -> StringTree

@external(erlang, "host", "is_equal")
pub fn is_equal(left: StringTree, right: StringTree) -> Bool

@external(erlang, "host", "is_empty")
pub fn is_empty(tree: StringTree) -> Bool
"#;

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

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

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

    #[test]
    fn executes_every_string_tree_provider_through_the_hosted_pipeline() {
        let execution = execution(
            r#"
pub fn main() {
  let segmented = from_strings(["a", "b"])
  let flat = from_string("ab")
  let joined = concat([segmented, from_string("c")])
  let appended = append_tree(joined, from_string("d"))
  assert segmented != flat
  assert is_equal(segmented, flat)
  assert to_string(appended) == "abcd"
  assert byte_size(appended) == 4
  assert to_string(lowercase(from_string("Gleam"))) == "gleam"
  assert to_string(uppercase(from_string("Gleam"))) == "GLEAM"
  assert do_to_graphemes("A👍🏽é") == ["A", "👍🏽", "é"]
  assert erl_split(from_string("a,b,c"), ",", All)
    == [from_string("a"), from_string("b"), from_string("c")]
  assert erl_split(from_string("abc"), "", All) == [from_string("abc")]
  assert to_string(replace(from_string("a-b-a"), "a", "x")) == "x-b-x"
  assert to_string(replace(from_string("abc"), "", "x")) == "abc"
  assert is_empty(from_strings([]))
  appended
}
"#,
        );
        let value = execution
            .run_main(
                &mut GleamStdlibRunState::from_seed([0; 32]),
                &mut Vec::new(),
            )
            .expect("string tree providers should run");

        assert_eq!(
            value.inspect().to_string(),
            r#"string_tree.from_string("abcd")"#,
        );
    }
}