Skip to main content

geam_core/planner/
host_requirement.rs

1use crate::frontend::TypedProgram;
2use ecow::EcoString;
3use gleam_compiler_core::ast::TypedFunction;
4use std::collections::BTreeSet;
5
6#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct RequiredHostFunction {
8    package: EcoString,
9    module: EcoString,
10    function: EcoString,
11}
12
13impl RequiredHostFunction {
14    pub fn package(&self) -> &EcoString {
15        &self.package
16    }
17
18    pub fn module(&self) -> &EcoString {
19        &self.module
20    }
21
22    pub fn function(&self) -> &EcoString {
23        &self.function
24    }
25}
26
27pub fn required_host_functions(program: &TypedProgram) -> Vec<RequiredHostFunction> {
28    program
29        .modules()
30        .flat_map(|module| {
31            module
32                .definitions
33                .functions
34                .iter()
35                .filter(|function| requires_erlang_host_provider(function))
36                .filter_map(|function| {
37                    function
38                        .name
39                        .as_ref()
40                        .map(|(_, name)| RequiredHostFunction {
41                            package: module.type_info.package.clone(),
42                            module: module.name.clone(),
43                            function: name.clone(),
44                        })
45                })
46        })
47        .collect::<BTreeSet<_>>()
48        .into_iter()
49        .collect()
50}
51
52pub(crate) fn requires_erlang_host_provider(function: &TypedFunction) -> bool {
53    function.external_erlang.is_some() && function.body.is_empty()
54}
55
56#[cfg(test)]
57mod tests {
58    use super::{RequiredHostFunction, required_host_functions};
59    use crate::frontend::{
60        ModuleSource, PackageSource, compile_typed_package_program, compile_typed_project,
61    };
62    use camino::Utf8PathBuf;
63    use std::fs;
64    use tempfile::tempdir;
65
66    #[test]
67    fn inventories_only_bodyless_erlang_externals_in_deterministic_order() {
68        let program = compile_typed_package_program(
69            "application",
70            "main",
71            [
72                PackageSource::new(
73                    "application",
74                    ["library"],
75                    [ModuleSource::new(
76                        "main",
77                        "main.gleam",
78                        r#"
79@external(erlang, "native", "root_required")
80fn root_required() -> Int
81
82pub fn main() {
83  1
84}
85"#,
86                    )],
87                ),
88                PackageSource::new(
89                    "library",
90                    Vec::<String>::new(),
91                    [
92                        ModuleSource::new(
93                            "zeta",
94                            "zeta.gleam",
95                            r#"
96@external(erlang, "native", "private_required")
97fn private_required() -> Int
98
99@external(erlang, "native", "fallback")
100pub fn fallback() -> Int {
101  1
102}
103
104@external(javascript, "./ffi.mjs", "javascript_only")
105fn javascript_only() -> Int
106
107pub type OrdinaryConstructorless
108"#,
109                        ),
110                        ModuleSource::new(
111                            "alpha",
112                            "alpha.gleam",
113                            r#"
114@external(erlang, "native", "dependency_required")
115pub fn dependency_required() -> Int
116"#,
117                        ),
118                    ],
119                ),
120            ],
121        )
122        .expect("host requirements should compile independently of providers");
123
124        assert_eq!(
125            required_host_functions(&program),
126            [
127                RequiredHostFunction {
128                    package: "application".into(),
129                    module: "main".into(),
130                    function: "root_required".into(),
131                },
132                RequiredHostFunction {
133                    package: "library".into(),
134                    module: "alpha".into(),
135                    function: "dependency_required".into(),
136                },
137                RequiredHostFunction {
138                    package: "library".into(),
139                    module: "zeta".into(),
140                    function: "private_required".into(),
141                },
142            ],
143        );
144    }
145
146    #[test]
147    fn exposes_owned_requirement_identity() {
148        let requirement = RequiredHostFunction {
149            package: "package".into(),
150            module: "module".into(),
151            function: "function".into(),
152        };
153
154        assert_eq!(requirement.package(), "package");
155        assert_eq!(requirement.module(), "module");
156        assert_eq!(requirement.function(), "function");
157    }
158
159    #[test]
160    fn follows_the_resolved_project_source_closure() {
161        let project = tempdir().expect("temporary project should be created");
162        let root = project.path();
163        fs::create_dir(root.join("src")).expect("source directory should be created");
164        fs::write(
165            root.join("gleam.toml"),
166            "name = \"application\"\nversion = \"1.0.0\"\n",
167        )
168        .expect("project config should be written");
169        fs::write(
170            root.join("manifest.toml"),
171            "packages = []\n\n[requirements]\n",
172        )
173        .expect("project manifest should be written");
174        fs::write(
175            root.join("src/main.gleam"),
176            "import used\npub fn main() { 1 }",
177        )
178        .expect("root module should be written");
179        fs::write(
180            root.join("src/used.gleam"),
181            r#"
182@external(erlang, "native", "used")
183pub fn used() -> Int
184"#,
185        )
186        .expect("selected module should be written");
187        fs::write(
188            root.join("src/unused.gleam"),
189            r#"
190@external(erlang, "native", "unused")
191pub fn unused() -> Int
192"#,
193        )
194        .expect("unselected module should be written");
195
196        let root = Utf8PathBuf::from_path_buf(root.to_path_buf())
197            .expect("temporary path should be valid UTF-8");
198        let program = compile_typed_project(root, "main")
199            .expect("resolved project source closure should compile");
200
201        assert_eq!(
202            program
203                .modules()
204                .map(|module| module.name.as_str())
205                .collect::<Vec<_>>(),
206            ["used", "main"],
207        );
208        assert_eq!(
209            required_host_functions(&program),
210            [RequiredHostFunction {
211                package: "application".into(),
212                module: "used".into(),
213                function: "used".into(),
214            }],
215        );
216    }
217}