geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
use crate::frontend::HostedTypedProgramModule;
use crate::host::{HostExternalTypeSchema, RegisteredHostFunction, RegisteredHostProviderModule};
use crate::plan::{ExternalTypeDefinition, ModuleId, SourceContext};
use crate::planner::error::{HostProviderLinkReason, PlanError};
use ecow::EcoString;
use gleam_core::ast::TypedFunction;
use std::collections::BTreeMap;

pub(super) enum HostedModuleDeclaration {
    Source {
        id: ModuleId,
        package: EcoString,
        module_name: EcoString,
        source_context: Option<SourceContext>,
        custom_types: Vec<crate::plan::CustomTypeDefinition>,
        external_types: Vec<ExternalTypeDefinition>,
        functions: Vec<TypedFunction>,
        constants: Vec<gleam_core::ast::TypedModuleConstant>,
        providers: Vec<RegisteredHostFunction>,
    },
    Host {
        id: ModuleId,
        package: EcoString,
        module_name: EcoString,
        functions: Vec<RegisteredHostFunction>,
    },
}

struct RegisteredProviderItems {
    functions: Vec<RegisteredHostFunction>,
    external_types: Vec<HostExternalTypeSchema>,
}

pub(super) fn collect_hosted_module_declarations(
    modules: Vec<HostedTypedProgramModule>,
    providers: Vec<RegisteredHostProviderModule>,
) -> Result<Vec<HostedModuleDeclaration>, PlanError> {
    let mut provider_modules = providers
        .into_iter()
        .map(|provider| {
            let (package, module, functions, external_types) = provider.into_parts();
            (
                (package, module),
                RegisteredProviderItems {
                    functions,
                    external_types,
                },
            )
        })
        .collect::<BTreeMap<_, _>>();
    let external_types = provider_modules
        .iter()
        .flat_map(|((package, module), items)| {
            items.external_types.iter().map(|schema| {
                crate::plan::ExternalTypeName::new(
                    package.clone(),
                    module.clone(),
                    schema.name().clone(),
                )
            })
        })
        .collect::<std::collections::HashSet<_>>();
    modules
        .into_iter()
        .enumerate()
        .map(|(index, module)| {
            hosted_module_declaration(
                ModuleId::new(index),
                module,
                &mut provider_modules,
                &external_types,
            )
        })
        .collect::<Result<Vec<_>, _>>()
        .and_then(|declarations| {
            if let Some(error) =
                provider_modules
                    .into_iter()
                    .find_map(|((package, module), items)| {
                        let function = items
                            .functions
                            .iter()
                            .map(|function| function.schema().name().clone())
                            .min();
                        if let Some(function) = function {
                            return Some(PlanError::HostProviderLink {
                                package,
                                module,
                                function,
                                reason: Box::new(HostProviderLinkReason::MissingModule),
                            });
                        }
                        items
                            .external_types
                            .iter()
                            .map(|schema| schema.name().clone())
                            .min()
                            .map(|type_| PlanError::ExternalTypeProviderLink {
                                package,
                                module,
                                type_,
                                reason: Box::new(
                                    crate::planner::ExternalTypeProviderLinkReason::MissingModule,
                                ),
                            })
                    })
            {
                Err(error)
            } else {
                Ok(declarations)
            }
        })
}

fn hosted_module_declaration(
    id: ModuleId,
    module: HostedTypedProgramModule,
    provider_modules: &mut BTreeMap<(EcoString, EcoString), RegisteredProviderItems>,
    external_types: &std::collections::HashSet<crate::plan::ExternalTypeName>,
) -> Result<HostedModuleDeclaration, PlanError> {
    match module {
        HostedTypedProgramModule::Source(module) => {
            let path = module.path;
            let source = module.source;
            let package = module.module.type_info.package.clone();
            let definitions = module.module.definitions;
            let module_name = module.module.name;
            let providers = provider_modules
                .remove(&(package.clone(), module_name.clone()))
                .unwrap_or(RegisteredProviderItems {
                    functions: Vec::new(),
                    external_types: Vec::new(),
                });
            super::super::external_type::plan_hosted_types(
                &package,
                &module_name,
                definitions.custom_types,
                providers.external_types,
                external_types,
            )
            .map(|types| HostedModuleDeclaration::Source {
                id,
                providers: providers.functions,
                package,
                module_name,
                source_context: Some(SourceContext::new(path, source)),
                custom_types: types.custom_types,
                external_types: types.external_types,
                functions: definitions.functions,
                constants: definitions.constants,
            })
        }
        HostedTypedProgramModule::Host(module) => {
            let (package, module_name, functions) = module.into_parts();
            Ok(HostedModuleDeclaration::Host {
                id,
                package,
                module_name,
                functions,
            })
        }
    }
}

#[cfg(test)]
mod tests {
    use super::super::plan_host_program;
    use crate::frontend::{ModuleSource, PackageSource, compile_typed_host_program};
    use crate::host::{HostModule, HostProviderModule, HostProviderSet, StatelessHostProfile};
    use crate::planner::{ExternalTypeProviderLinkReason, HostProviderLinkReason, PlanError};
    use ecow::EcoString;
    use num_bigint::BigInt;

    #[test]
    fn reject_profile_host_program_custom_declaration_precedence() {
        let typed = compile_typed_host_program(
            "application",
            "main",
            [PackageSource::new(
                "application",
                Vec::<EcoString>::new(),
                [
                    ModuleSource::new("main", "main.gleam", "pub fn other() { 1 }"),
                    ModuleSource::new(
                        "zsupport",
                        "zsupport.gleam",
                        r#"
@external(erlang, "external", "thing")
pub type Thing

pub fn value() { 1 }
"#,
                    ),
                ],
            )],
            HostProviderSet::new(Vec::<HostModule>::new())
                .expect("empty host modules should be valid"),
        )
        .expect("profile-out source should still compile");

        assert_eq!(
            plan_host_program(typed).err(),
            Some(PlanError::ExternalTypeProviderLink {
                package: "application".into(),
                module: "zsupport".into(),
                type_: "Thing".into(),
                reason: Box::new(ExternalTypeProviderLinkReason::MissingRegistration),
            }),
        );
    }

    #[test]
    fn provider_module_must_link_to_a_source_module() {
        let provider = HostProviderModule::<StatelessHostProfile>::new("application", "missing")
            .expect("provider module should be valid")
            .with_function("native", BigInt::default)
            .expect("provider function should be valid");
        let typed = compile_typed_host_program(
            "application",
            "main",
            [PackageSource::new(
                "application",
                Vec::<EcoString>::new(),
                [ModuleSource::new(
                    "main",
                    "main.gleam",
                    "pub fn main() { 1 }",
                )],
            )],
            HostProviderSet::with_providers(Vec::<HostModule>::new(), [provider])
                .expect("provider module should be unique"),
        )
        .expect("host program should compile");

        assert_eq!(
            plan_host_program(typed).err(),
            Some(PlanError::HostProviderLink {
                package: "application".into(),
                module: "missing".into(),
                function: "native".into(),
                reason: Box::new(HostProviderLinkReason::MissingModule),
            }),
        );
    }

    #[test]
    fn empty_provider_module_has_no_source_linkage_target() {
        let provider = HostProviderModule::<StatelessHostProfile>::new("application", "missing")
            .expect("provider module should be valid");
        let typed = compile_typed_host_program(
            "application",
            "main",
            [PackageSource::new(
                "application",
                Vec::<EcoString>::new(),
                [ModuleSource::new(
                    "main",
                    "main.gleam",
                    "pub fn main() { 1 }",
                )],
            )],
            HostProviderSet::with_providers(Vec::<HostModule>::new(), [provider])
                .expect("provider module should be unique"),
        )
        .expect("host program should compile");

        let plan = plan_host_program(typed).expect("empty provider should not require a module");

        assert_eq!(plan.modules().len(), 1);
        assert_eq!(plan.modules()[0].module(), "main");
    }

    #[test]
    fn missing_provider_module_reports_the_lexically_first_function() {
        let provider = HostProviderModule::<StatelessHostProfile>::new("application", "missing")
            .expect("provider module should be valid")
            .with_function("zeta", BigInt::default)
            .expect("provider function should be valid")
            .with_function("alpha", BigInt::default)
            .expect("provider function should be valid");
        let typed = compile_typed_host_program(
            "application",
            "main",
            [PackageSource::new(
                "application",
                Vec::<EcoString>::new(),
                [ModuleSource::new(
                    "main",
                    "main.gleam",
                    "pub fn main() { 1 }",
                )],
            )],
            HostProviderSet::with_providers(Vec::<HostModule>::new(), [provider])
                .expect("provider module should be unique"),
        )
        .expect("host program should compile");

        assert_eq!(
            plan_host_program(typed).err(),
            Some(PlanError::HostProviderLink {
                package: "application".into(),
                module: "missing".into(),
                function: "alpha".into(),
                reason: Box::new(HostProviderLinkReason::MissingModule),
            }),
        );
    }
}