Skip to main content

lust/
externs.rs

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    /// When an export name is not module-qualified (has no '.' or '::'),
15    /// place it into this module.
16    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(&params);
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(&params);
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