1#![cfg(feature = "std")]
2
3use crate::embed::native_types::ModuleStub;
4use crate::{NativeExport, VM};
5use std::{
6 collections::BTreeMap,
7 fs,
8 io,
9 path::{Path, PathBuf},
10};
11
12#[derive(Debug, Clone, Default)]
13pub struct DumpExternsOptions {
14 pub default_module: Option<String>,
17}
18
19#[derive(Debug, Clone)]
20pub struct ExternFile {
21 pub relative_path: PathBuf,
22 pub contents: String,
23}
24
25pub fn extern_files_from_vm(vm: &VM, options: &DumpExternsOptions) -> Vec<ExternFile> {
26 extern_files_from_exports(vm.exported_natives(), vm.exported_type_stubs(), options)
27}
28
29pub fn write_extern_files(
30 output_root: impl AsRef<Path>,
31 files: &[ExternFile],
32) -> io::Result<Vec<PathBuf>> {
33 let output_root = output_root.as_ref();
34 if files.is_empty() {
35 return Ok(Vec::new());
36 }
37 fs::create_dir_all(output_root)?;
38
39 let mut written = Vec::with_capacity(files.len());
40 for file in files {
41 let mut relative = file.relative_path.clone();
42 if relative.extension().is_none() {
43 relative.set_extension("lust");
44 }
45 let destination = output_root.join(&relative);
46 if let Some(parent) = destination.parent() {
47 fs::create_dir_all(parent)?;
48 }
49 fs::write(&destination, &file.contents)?;
50 written.push(relative);
51 }
52
53 Ok(written)
54}
55
56fn extern_files_from_exports(
57 exports: &[NativeExport],
58 type_stubs: &[ModuleStub],
59 options: &DumpExternsOptions,
60) -> Vec<ExternFile> {
61 if exports.is_empty() && type_stubs.iter().all(ModuleStub::is_empty) {
62 return Vec::new();
63 }
64
65 #[derive(Default)]
66 struct CombinedModule<'a> {
67 type_stub: ModuleStub,
68 functions: Vec<&'a NativeExport>,
69 }
70
71 let mut combined: BTreeMap<String, CombinedModule<'_>> = BTreeMap::new();
72 for stub in type_stubs {
73 if stub.is_empty() {
74 continue;
75 }
76 let entry = combined
77 .entry(stub.module.clone())
78 .or_insert_with(|| CombinedModule {
79 type_stub: ModuleStub {
80 module: stub.module.clone(),
81 ..ModuleStub::default()
82 },
83 ..CombinedModule::default()
84 });
85 entry.type_stub.struct_defs.extend(stub.struct_defs.clone());
86 entry.type_stub.enum_defs.extend(stub.enum_defs.clone());
87 entry.type_stub.trait_defs.extend(stub.trait_defs.clone());
88 entry.type_stub.const_defs.extend(stub.const_defs.clone());
89 }
90
91 for export in exports {
92 let normalized_name = export.name().replace("::", ".");
93 let (module, function) = match normalized_name.rsplit_once('.') {
94 Some(parts) => (parts.0.to_string(), parts.1.to_string()),
95 None => match &options.default_module {
96 Some(default) if !default.trim().is_empty() => (default.clone(), normalized_name),
97 _ => continue,
98 },
99 };
100 let entry = combined
101 .entry(module.clone())
102 .or_insert_with(|| CombinedModule {
103 type_stub: ModuleStub {
104 module,
105 ..ModuleStub::default()
106 },
107 ..CombinedModule::default()
108 });
109 if function.is_empty() {
110 continue;
111 }
112 entry.functions.push(export);
113 }
114
115 let mut result = Vec::new();
116 for (module, mut combined_entry) in combined {
117 combined_entry
118 .functions
119 .sort_by(|a, b| a.name().cmp(b.name()));
120 let mut contents = String::new();
121
122 let mut wrote_type = false;
123 let append_defs = |defs: &Vec<String>, contents: &mut String, wrote_flag: &mut bool| {
124 if defs.is_empty() {
125 return;
126 }
127 if *wrote_flag && !contents.ends_with("\n\n") && !contents.is_empty() {
128 contents.push('\n');
129 }
130 for def in defs {
131 contents.push_str(def);
132 if !def.ends_with('\n') {
133 contents.push('\n');
134 }
135 }
136 *wrote_flag = true;
137 };
138
139 append_defs(
140 &combined_entry.type_stub.struct_defs,
141 &mut contents,
142 &mut wrote_type,
143 );
144 append_defs(
145 &combined_entry.type_stub.enum_defs,
146 &mut contents,
147 &mut wrote_type,
148 );
149 append_defs(
150 &combined_entry.type_stub.trait_defs,
151 &mut contents,
152 &mut wrote_type,
153 );
154 append_defs(
155 &combined_entry.type_stub.const_defs,
156 &mut contents,
157 &mut wrote_type,
158 );
159
160 if !combined_entry.functions.is_empty() {
161 if wrote_type && !contents.ends_with("\n\n") {
162 contents.push('\n');
163 }
164 contents.push_str("pub extern\n");
165 for export in combined_entry.functions {
166 let normalized_name = export.name().replace("::", ".");
167 if let Some((_, function)) = normalized_name.rsplit_once('.') {
168 let params = format_params(export);
169 let return_type = export.return_type();
170 if let Some(doc) = export.doc() {
171 contents.push_str(" -- ");
172 contents.push_str(doc);
173 if !doc.ends_with('\n') {
174 contents.push('\n');
175 }
176 }
177 contents.push_str(" function ");
178 contents.push_str(function);
179 contents.push('(');
180 contents.push_str(¶ms);
181 contents.push(')');
182 if !return_type.trim().is_empty() && return_type.trim() != "()" {
183 contents.push_str(": ");
184 contents.push_str(return_type);
185 }
186 contents.push('\n');
187 } else if let Some(default) = &options.default_module {
188 if default == &module {
189 let params = format_params(export);
190 let return_type = export.return_type();
191 contents.push_str(" function ");
192 contents.push_str(&normalized_name);
193 contents.push('(');
194 contents.push_str(¶ms);
195 contents.push(')');
196 if !return_type.trim().is_empty() && return_type.trim() != "()" {
197 contents.push_str(": ");
198 contents.push_str(return_type);
199 }
200 contents.push('\n');
201 }
202 }
203 }
204 contents.push_str("end\n");
205 }
206
207 if contents.is_empty() {
208 continue;
209 }
210 let mut relative = relative_stub_path(&module);
211 if relative.extension().is_none() {
212 relative.set_extension("lust");
213 }
214 result.push(ExternFile {
215 relative_path: relative,
216 contents,
217 });
218 }
219
220 result
221}
222
223fn format_params(export: &NativeExport) -> String {
224 export
225 .params()
226 .iter()
227 .map(|param| {
228 let ty = param.ty().trim();
229 if ty.is_empty() {
230 "any"
231 } else {
232 ty
233 }
234 })
235 .collect::<Vec<_>>()
236 .join(", ")
237}
238
239fn relative_stub_path(module: &str) -> PathBuf {
240 let mut path = PathBuf::new();
241 let mut segments: Vec<String> = module.split('.').map(|seg| seg.replace('-', "_")).collect();
242 if let Some(first) = segments.first() {
243 if first == "externs" {
244 segments.remove(0);
245 }
246 }
247 if let Some(first) = segments.first() {
248 path.push(first);
249 }
250 if segments.len() > 1 {
251 for seg in &segments[1..segments.len() - 1] {
252 path.push(seg);
253 }
254 path.push(segments.last().unwrap());
255 } else if let Some(first) = segments.first() {
256 path.push(first);
257 }
258 path
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use crate::{LustConfig, NativeExportParam};
265 use tempfile::tempdir;
266
267 #[test]
268 fn writes_vm_exports_and_type_stubs() {
269 let mut vm = VM::with_config(&LustConfig::default());
270 vm.register_type_stubs(vec![ModuleStub {
271 module: "host".to_string(),
272 struct_defs: vec!["pub struct Widget\nend\n".to_string()],
273 ..ModuleStub::default()
274 }]);
275 vm.record_exported_native(NativeExport::new(
276 "host.scale",
277 vec![NativeExportParam::new("value", "int")],
278 "int",
279 ));
280
281 let dir = tempdir().expect("temp dir");
282 let files = extern_files_from_vm(&vm, &DumpExternsOptions::default());
283 assert_eq!(files.len(), 1);
284 let written = write_extern_files(dir.path(), &files).expect("write externs");
285 assert_eq!(written.len(), 1);
286 let destination = dir.path().join(&written[0]);
287 let contents = fs::read_to_string(destination).expect("read output");
288 assert!(contents.contains("pub struct Widget"));
289 assert!(contents.contains("pub extern"));
290 assert!(contents.contains("function scale(int): int"));
291 }
292}
293