geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
use super::RuntimeHostCall;
use crate::plan::execution::function::{ExecutionFunctionBody, FunctionBodyOwner};
use crate::plan::execution::runtime::RuntimeExecutionPlan;
use crate::runtime::ExecutionError;
use crate::runtime::error::{ExecutionResult, HostCallOrigin};
use crate::runtime::graph::{GraphValue, RetainedValues};
use crate::runtime::state::RuntimeStateFor;

pub(in crate::runtime) fn invoke_value<'run, Profile, Body>(
    plan: &crate::plan::execution::HostedExecution<Profile>,
    state: &mut RuntimeStateFor<'run, crate::plan::execution::HostedExecution<Profile>>,
    origin: HostCallOrigin,
    target: &crate::plan::execution::host::HostFunctionId<Body>,
    inputs: RetainedValues,
) -> ExecutionResult<<<Body as FunctionBodyOwner>::Return as GraphValue>::Evaluated>
where
    Profile: crate::HostProfile,
    Body: ExecutionFunctionBody,
    Body::Return: GraphValue,
    crate::plan::execution::HostedExecution<Profile>: 'run,
{
    let function = plan.host_value_function(target);
    let mut call = RuntimeHostCall::new(plan, state, function, inputs);
    match function.implementation().call(&mut call) {
        Ok(returned) => Ok(call.finish(returned, target.return_())),
        Err(error) => {
            drop(call);
            state.lists_mut().drain_releases();
            Err(host_call_error(plan, origin, function.metadata(), error))
        }
    }
}

pub(in crate::runtime) fn invoke_never<'run, Profile>(
    plan: &crate::plan::execution::HostedExecution<Profile>,
    state: &mut RuntimeStateFor<'run, crate::plan::execution::HostedExecution<Profile>>,
    origin: HostCallOrigin,
    target: crate::plan::execution::host::HostNeverFunctionId,
    inputs: RetainedValues,
) -> ExecutionResult<std::convert::Infallible>
where
    Profile: crate::HostProfile,
    crate::plan::execution::HostedExecution<Profile>: 'run,
{
    let function = plan.host_never_function(target);
    let mut call = RuntimeHostCall::new(plan, state, function, inputs);
    match function.implementation().call(&mut call) {
        Ok(never) => match never {},
        Err(error) => {
            drop(call);
            state.lists_mut().drain_releases();
            Err(host_call_error(plan, origin, function.metadata(), error))
        }
    }
}

fn host_call_error<Profile: crate::HostProfile>(
    plan: &crate::plan::execution::HostedExecution<Profile>,
    origin: HostCallOrigin,
    function: &crate::plan::execution::host::HostedFunctionMetadata,
    error: crate::host::HostCallError,
) -> ExecutionError {
    match error.into_kind() {
        crate::host::HostCallErrorKind::Nested(error) => error,
        crate::host::HostCallErrorKind::Failure(failure) => {
            match origin.into_source_site(function.site()) {
                Ok(site) => ExecutionError::from_host_call(
                    function,
                    site.clone(),
                    plan.source_context_for(site.module()),
                    failure,
                ),
                Err(caller) => ExecutionError::from_host_origin(function, caller, failure),
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::host::{ExternalTestProfile, ExternalTestRunState};
    use crate::runtime::profile::external_test::{
        RuntimeCounterCallable, RuntimeCounterProvider, RuntimeCounterSchema, RuntimeHostCounter,
    };
    use crate::{
        HostCall, HostCallError, HostFailure, HostListType, HostModule, HostProviderModule,
        HostProviderSet, HostedExecution, ModuleSource, PackageSource, compile_typed_host_program,
        plan_host_program,
    };
    use ecow::EcoString;
    use std::convert::Infallible;

    #[test]
    fn hosted_runtime_routes_diverging_external_return_families() {
        fn stop_counter<'call>(
            _call: HostCall<'call, ExternalTestProfile, RuntimeCounterProvider, RuntimeHostCounter>,
        ) -> Result<Infallible, HostCallError> {
            Err(HostFailure::new("counter stopped").into())
        }

        fn stop_list<'call>(
            _call: HostCall<
                'call,
                ExternalTestProfile,
                RuntimeCounterProvider,
                HostListType<RuntimeHostCounter>,
            >,
        ) -> Result<Infallible, HostCallError> {
            Err(HostFailure::new("list stopped").into())
        }

        fn stop_function<'call>(
            _call: HostCall<
                'call,
                ExternalTestProfile,
                RuntimeCounterProvider,
                RuntimeCounterCallable,
            >,
        ) -> Result<Infallible, HostCallError> {
            Err(HostFailure::new("function stopped").into())
        }

        enum ReturnFamily {
            Value,
            List,
            Function,
        }

        let cases = [
            (
                ReturnFamily::Value,
                r#"
@external(erlang, "host", "Counter")
pub type Counter

@external(erlang, "host", "stop_counter")
fn stop_counter() -> Counter

fn forward() -> Counter {
  stop_counter()
}

pub fn main() {
  let _ = forward()
  1
}
"#,
                "host function application::main.stop_counter failed: counter stopped",
            ),
            (
                ReturnFamily::List,
                r#"
@external(erlang, "host", "Counter")
pub type Counter

@external(erlang, "host", "stop_list")
fn stop_list() -> List(Counter)

fn forward() -> List(Counter) {
  stop_list()
}

pub fn main() {
  let _ = forward()
  1
}
"#,
                "host function application::main.stop_list failed: list stopped",
            ),
            (
                ReturnFamily::Function,
                r#"
@external(erlang, "host", "Counter")
pub type Counter

@external(erlang, "host", "stop_function")
fn stop_function() -> fn(Int) -> Counter

fn forward() -> fn(Int) -> Counter {
  stop_function()
}

pub fn main() {
  let _ = forward()
  1
}
"#,
                "host function application::main.stop_function failed: function stopped",
            ),
        ];

        for (family, source, expected_error) in cases {
            let provider = HostProviderModule::<ExternalTestProfile>::new("application", "main")
                .expect("provider module should be valid");
            let provider = match family {
                ReturnFamily::Value => provider
                    .with_external_type::<RuntimeCounterProvider, RuntimeCounterSchema>()
                    .expect("external type should be valid")
                    .with_scoped_diverging_function::<
                        RuntimeCounterProvider,
                        (),
                        RuntimeHostCounter,
                        _,
                    >(
                        "stop_counter",
                        stop_counter,
                    )
                    .expect("external value target should be valid"),
                ReturnFamily::List => provider
                    .with_external_type::<RuntimeCounterProvider, RuntimeCounterSchema>()
                    .expect("external type should be valid")
                    .with_scoped_diverging_function::<
                        RuntimeCounterProvider,
                        (),
                        HostListType<RuntimeHostCounter>,
                        _,
                    >("stop_list", stop_list)
                    .expect("external list target should be valid"),
                ReturnFamily::Function => provider
                    .with_external_type::<RuntimeCounterProvider, RuntimeCounterSchema>()
                    .expect("external type should be valid")
                    .with_scoped_diverging_function::<
                        RuntimeCounterProvider,
                        (),
                        RuntimeCounterCallable,
                        _,
                    >(
                        "stop_function",
                        stop_function,
                    )
                    .expect("external function target should be valid"),
            };
            let typed = compile_typed_host_program(
                "application",
                "main",
                [PackageSource::new(
                    "application",
                    Vec::<EcoString>::new(),
                    [ModuleSource::new("main", "src/main.gleam", source)],
                )],
                HostProviderSet::with_providers(
                    Vec::<HostModule<ExternalTestProfile>>::new(),
                    [provider],
                )
                .expect("provider module should be unique"),
            )
            .expect("diverging external source should compile");
            let plan = plan_host_program(typed).expect("diverging external source should plan");
            let execution = HostedExecution::try_from_module_plan(plan)
                .expect("diverging external execution should seal");
            let error = execution
                .run_main(&mut ExternalTestRunState::default(), &mut Vec::new())
                .expect_err("diverging external target should fail");

            assert_eq!(error.to_string(), expected_error);
        }
    }
}