geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
use super::{IoOutput, IoSink, IoStream};
use crate::gleam_stdlib::GleamStdlibHostProfile;
use crate::{HostCall, HostCallCompletion, HostCallError, HostProvider};
use ecow::EcoString;
use std::marker::PhantomData;

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

impl<Profile> HostProvider<Profile> for IoProvider<Profile>
where
    Profile: GleamStdlibHostProfile,
{
    type State = Profile::Io;

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

pub(super) fn print<'call, Profile>(
    call: HostCall<'call, Profile, IoProvider<Profile>, ()>,
    text: EcoString,
) -> Result<HostCallCompletion<'call, ()>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    emit(call, IoStream::Stdout, text)
}

pub(super) fn print_error<'call, Profile>(
    call: HostCall<'call, Profile, IoProvider<Profile>, ()>,
    text: EcoString,
) -> Result<HostCallCompletion<'call, ()>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    emit(call, IoStream::Stderr, text)
}

pub(super) fn println<'call, Profile>(
    call: HostCall<'call, Profile, IoProvider<Profile>, ()>,
    mut text: EcoString,
) -> Result<HostCallCompletion<'call, ()>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    text.push('\n');
    emit(call, IoStream::Stdout, text)
}

pub(super) fn println_error<'call, Profile>(
    call: HostCall<'call, Profile, IoProvider<Profile>, ()>,
    mut text: EcoString,
) -> Result<HostCallCompletion<'call, ()>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    text.push('\n');
    emit(call, IoStream::Stderr, text)
}

fn emit<'call, Profile>(
    mut call: HostCall<'call, Profile, IoProvider<Profile>, ()>,
    stream: IoStream,
    text: EcoString,
) -> Result<HostCallCompletion<'call, ()>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    call.state().emit(IoOutput::new(stream, text));
    Ok(call.return_value(()))
}

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

    const IO_SOURCE: &str = r#"
@external(erlang, "gleam_stdlib", "print")
pub fn print(string: String) -> Nil

@external(erlang, "gleam_stdlib", "print_error")
pub fn print_error(string: String) -> Nil

@external(erlang, "gleam_stdlib", "println")
pub fn println(string: String) -> Nil

@external(erlang, "gleam_stdlib", "println_error")
pub fn println_error(string: String) -> Nil

pub fn main() {
  print("first")
  print_error("second")
  println("third")
  println_error("fourth")
}
"#;

    #[test]
    fn executes_all_io_callbacks_through_the_typed_provider() {
        let provider = super::super::host_provider::<GleamStdlibProfile>()
            .expect("synthetic IO provider should register");
        let typed = compile_typed_host_program(
            "gleam_stdlib",
            "gleam/io",
            [PackageSource::new(
                "gleam_stdlib",
                Vec::<EcoString>::new(),
                [ModuleSource::new(
                    "gleam/io",
                    "src/gleam/io.gleam",
                    IO_SOURCE,
                )],
            )],
            HostProviderSet::with_providers(
                Vec::<HostModule<GleamStdlibProfile>>::new(),
                [provider],
            )
            .expect("synthetic IO provider module should be unique"),
        )
        .expect("synthetic IO source should compile");
        let plan = plan_host_program(typed).expect("synthetic IO source should plan");
        let execution =
            HostedExecution::try_from_module_plan(plan).expect("synthetic IO source should seal");
        let mut state = GleamStdlibRunState::from_seed([2; 32]);

        assert_eq!(
            execution
                .run_main(&mut state, &mut Vec::new())
                .expect("synthetic IO source should run"),
            Value::Nil,
        );
        assert_eq!(
            state
                .io_outputs()
                .iter()
                .map(|output| (output.stream(), output.text().as_str()))
                .collect::<Vec<_>>(),
            [
                (IoStream::Stdout, "first"),
                (IoStream::Stderr, "second"),
                (IoStream::Stdout, "third\n"),
                (IoStream::Stderr, "fourth\n"),
            ],
        );
    }
}