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}