geam 0.1.2

Experimental Rust-embedded execution runtime for typed Gleam programs
use crate::frontend::TypedProgram;
use ecow::EcoString;
use gleam_core::ast::TypedFunction;
use std::collections::BTreeSet;

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RequiredHostFunction {
    package: EcoString,
    module: EcoString,
    function: EcoString,
}

impl RequiredHostFunction {
    pub fn package(&self) -> &EcoString {
        &self.package
    }

    pub fn module(&self) -> &EcoString {
        &self.module
    }

    pub fn function(&self) -> &EcoString {
        &self.function
    }
}

pub fn required_host_functions(program: &TypedProgram) -> Vec<RequiredHostFunction> {
    program
        .modules()
        .flat_map(|module| {
            module
                .definitions
                .functions
                .iter()
                .filter(|function| requires_erlang_host_provider(function))
                .filter_map(|function| {
                    function
                        .name
                        .as_ref()
                        .map(|(_, name)| RequiredHostFunction {
                            package: module.type_info.package.clone(),
                            module: module.name.clone(),
                            function: name.clone(),
                        })
                })
        })
        .collect::<BTreeSet<_>>()
        .into_iter()
        .collect()
}

pub(crate) fn requires_erlang_host_provider(function: &TypedFunction) -> bool {
    function.external_erlang.is_some() && function.body.is_empty()
}

#[cfg(test)]
mod tests {
    use super::{RequiredHostFunction, required_host_functions};
    use crate::frontend::{
        ModuleSource, PackageSource, compile_typed_package_program, compile_typed_project,
    };
    use camino::Utf8PathBuf;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn inventories_only_bodyless_erlang_externals_in_deterministic_order() {
        let program = compile_typed_package_program(
            "application",
            "main",
            [
                PackageSource::new(
                    "application",
                    ["library"],
                    [ModuleSource::new(
                        "main",
                        "main.gleam",
                        r#"
@external(erlang, "native", "root_required")
fn root_required() -> Int

pub fn main() {
  1
}
"#,
                    )],
                ),
                PackageSource::new(
                    "library",
                    Vec::<String>::new(),
                    [
                        ModuleSource::new(
                            "zeta",
                            "zeta.gleam",
                            r#"
@external(erlang, "native", "private_required")
fn private_required() -> Int

@external(erlang, "native", "fallback")
pub fn fallback() -> Int {
  1
}

@external(javascript, "./ffi.mjs", "javascript_only")
fn javascript_only() -> Int

pub type OrdinaryConstructorless
"#,
                        ),
                        ModuleSource::new(
                            "alpha",
                            "alpha.gleam",
                            r#"
@external(erlang, "native", "dependency_required")
pub fn dependency_required() -> Int
"#,
                        ),
                    ],
                ),
            ],
        )
        .expect("host requirements should compile independently of providers");

        assert_eq!(
            required_host_functions(&program),
            [
                RequiredHostFunction {
                    package: "application".into(),
                    module: "main".into(),
                    function: "root_required".into(),
                },
                RequiredHostFunction {
                    package: "library".into(),
                    module: "alpha".into(),
                    function: "dependency_required".into(),
                },
                RequiredHostFunction {
                    package: "library".into(),
                    module: "zeta".into(),
                    function: "private_required".into(),
                },
            ],
        );
    }

    #[test]
    fn exposes_owned_requirement_identity() {
        let requirement = RequiredHostFunction {
            package: "package".into(),
            module: "module".into(),
            function: "function".into(),
        };

        assert_eq!(requirement.package(), "package");
        assert_eq!(requirement.module(), "module");
        assert_eq!(requirement.function(), "function");
    }

    #[test]
    fn follows_the_resolved_project_source_closure() {
        let project = tempdir().expect("temporary project should be created");
        let root = project.path();
        fs::create_dir(root.join("src")).expect("source directory should be created");
        fs::write(
            root.join("gleam.toml"),
            "name = \"application\"\nversion = \"1.0.0\"\n",
        )
        .expect("project config should be written");
        fs::write(
            root.join("manifest.toml"),
            "packages = []\n\n[requirements]\n",
        )
        .expect("project manifest should be written");
        fs::write(
            root.join("src/main.gleam"),
            "import used\npub fn main() { 1 }",
        )
        .expect("root module should be written");
        fs::write(
            root.join("src/used.gleam"),
            r#"
@external(erlang, "native", "used")
pub fn used() -> Int
"#,
        )
        .expect("selected module should be written");
        fs::write(
            root.join("src/unused.gleam"),
            r#"
@external(erlang, "native", "unused")
pub fn unused() -> Int
"#,
        )
        .expect("unselected module should be written");

        let root = Utf8PathBuf::from_path_buf(root.to_path_buf())
            .expect("temporary path should be valid UTF-8");
        let program = compile_typed_project(root, "main")
            .expect("resolved project source closure should compile");

        assert_eq!(
            program
                .modules()
                .map(|module| module.name.as_str())
                .collect::<Vec<_>>(),
            ["used", "main"],
        );
        assert_eq!(
            required_host_functions(&program),
            [RequiredHostFunction {
                package: "application".into(),
                module: "used".into(),
                function: "used".into(),
            }],
        );
    }
}