moonbit_component_generator/
lib.rs

1use anyhow::{Context, anyhow};
2use camino::{Utf8Path, Utf8PathBuf};
3use camino_tempfile::Utf8TempDir;
4use heck::{ToLowerCamelCase, ToSnakeCase};
5use include_dir::{Dir, include_dir};
6use lazy_static::lazy_static;
7use log::debug;
8use log::info;
9use serde::Deserialize;
10use std::collections::{BTreeMap, HashSet};
11use std::fmt::Display;
12use std::ops::Range;
13use std::sync::atomic::{AtomicBool, Ordering};
14use topologic::AcyclicDependencyGraph;
15use wit_component::{ComponentEncoder, StringEncoding};
16use wit_parser::{PackageId, PackageName, Resolve, WorldId};
17
18mod moonc_wasm;
19
20/// An example generator that embeds and exports a script (arbitrary string) into a MoonBit component.
21#[cfg(feature = "get-script")]
22pub mod get_script;
23
24/// An example generator that implements a simple typed configuration interface defined in WIT
25#[cfg(feature = "typed-config")]
26pub mod typed_config;
27
28static MOONBIT_CORE: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/bundled-core");
29
30#[derive(Default)]
31struct MoonC {
32    initialized: AtomicBool,
33}
34
35impl MoonC {
36    pub fn run(&self, mut args: Vec<String>) -> anyhow::Result<()> {
37        self.ensure_initialized()?;
38        debug!("Running the MoonBit compiler with args: {}", args.join(" "));
39        args.insert(0, "moonc".to_string());
40        moonc_wasm::run_wasmoo(args).context("Running the MoonBit compiler")?;
41        Ok(())
42    }
43
44    fn ensure_initialized(&self) -> anyhow::Result<()> {
45        if !self.initialized.load(Ordering::Acquire) {
46            debug!("Initializing V8...");
47            moonc_wasm::initialize_v8()?;
48            self.initialized.store(true, Ordering::Release);
49        }
50        Ok(())
51    }
52}
53
54lazy_static! {
55    static ref MOONC: MoonC = MoonC::default();
56}
57
58pub struct MoonBitComponent {
59    dir: Utf8PathBuf,
60    temp: Option<Utf8TempDir>,
61    packages: BTreeMap<String, MoonBitPackage>,
62    resolve: Option<Resolve>,
63    world_id: Option<WorldId>,
64    root_package_id: Option<PackageId>,
65}
66
67impl MoonBitComponent {
68    /// Initializes a new MoonBit component that implements the given WIT interface.
69    ///
70    /// This step will create a temporary directory and generate MoonBit WIT bindings in it.
71    pub fn empty_from_wit(
72        wit: impl AsRef<str>,
73        selected_world: Option<&str>,
74    ) -> anyhow::Result<Self> {
75        let temp_dir = Utf8TempDir::new().context("Creating temporary directory")?;
76        let dir = temp_dir.path().to_path_buf();
77
78        info!("Creating MoonBit component in temporary directory: {dir}");
79
80        let mut component = MoonBitComponent {
81            dir,
82            temp: Some(temp_dir),
83            packages: BTreeMap::new(),
84            resolve: None,
85            world_id: None,
86            root_package_id: None,
87        };
88
89        info!("Saving WIT package to {}/package.wit", component.wit_dir());
90        std::fs::create_dir_all(component.wit_dir()).context("Creating WIT package directory")?;
91        std::fs::write(
92            component.wit_dir().join("package.wit"),
93            wit.as_ref().as_bytes(),
94        )?;
95
96        info!("Resolving WIT package");
97        let mut resolve = Resolve::default();
98        let (root_package_id, _) = resolve
99            .push_dir(component.wit_dir())
100            .context("Resolving WIT packages")?;
101        let world_id = resolve
102            .select_world(root_package_id, selected_world)
103            .context("Selecting the WIT world")?;
104
105        info!("Generating MoonBit WIT bindings");
106        let mut wit_bindgen = wit_bindgen_moonbit::Opts {
107            gen_dir: "gen".to_string(),
108            derive_eq: true,
109            derive_show: true,
110            ..Default::default()
111        }
112        .build();
113        let mut bindgen_files = wit_bindgen_core::Files::default();
114        wit_bindgen
115            .generate(&resolve, world_id, &mut bindgen_files)
116            .context("Generating MoonBit WIT bindings")?;
117
118        for (name, contents) in bindgen_files.iter() {
119            let dst = if let Some(stripped_name) = name.strip_prefix('/') {
120                component.dir.join(stripped_name)
121            } else {
122                component.dir.join(name)
123            };
124            debug!("Writing binding file {dst}");
125
126            if let Some(parent) = dst.parent() {
127                std::fs::create_dir_all(parent)
128                    .context("Creating directory for generated MoonBit WIT bindings")?;
129            }
130            std::fs::write(&dst, contents).context("Writing generated MoonBit WIT bindings")?;
131        }
132
133        component.extract_core()?;
134
135        component.resolve = Some(resolve);
136        component.world_id = Some(world_id);
137        component.root_package_id = Some(root_package_id);
138        Ok(component)
139    }
140
141    /// Disables cleaning up the temporary directory, for debugging purposes.
142    pub fn disable_cleanup(&mut self) {
143        if let Some(temp) = &mut self.temp {
144            temp.disable_cleanup(true);
145        }
146    }
147
148    /// Initializes a new MoonBit component from an existing directory with a valid 'wit' directory in it.
149    ///
150    /// The existing directory is expected to have the WIT bindings already generated.
151    pub fn existing(path: &Utf8Path, selected_world: Option<&str>) -> anyhow::Result<Self> {
152        let mut component = MoonBitComponent {
153            dir: path.to_path_buf(),
154            temp: None,
155            packages: BTreeMap::new(),
156            resolve: None,
157            world_id: None,
158            root_package_id: None,
159        };
160        component.extract_core()?;
161
162        info!("Resolving WIT package");
163        let mut resolve = Resolve::default();
164        let (root_package_id, _) = resolve
165            .push_dir(component.wit_dir())
166            .context("Resolving WIT package")?;
167        let world_id = resolve
168            .select_world(root_package_id, selected_world)
169            .context("Selecting WIT world")?;
170
171        component.resolve = Some(resolve);
172        component.world_id = Some(world_id);
173        component.root_package_id = Some(root_package_id);
174
175        Ok(component)
176    }
177
178    /// Defines the MoonBit packages implementing the WIT bindings
179    pub fn define_bindgen_packages(&mut self) -> anyhow::Result<()> {
180        let moonbit_root_package = self.moonbit_root_package()?;
181        let world_name = self.world_name()?;
182        let world_snake = to_moonbit_ident(&world_name);
183
184        let imported_interfaces = self.get_imported_interfaces()?;
185        let exported_interfaces = self.get_exported_interfaces()?;
186
187        debug!("Imported interfaces: {imported_interfaces:?}");
188        debug!("Exported interfaces: {exported_interfaces:?}");
189
190        let mut gen_dependencies = Vec::new();
191        let mut gen_mbt_files = Vec::new();
192
193        self.define_package(MoonBitPackage {
194            name: format!("{moonbit_root_package}/ffi"),
195            mbt_files: vec![Utf8Path::new("ffi").join("top.mbt")],
196            warning_control: vec![WarningControl::Disable(Warning::Specific(44))],
197            alert_control: vec![],
198            output: Utf8Path::new("target")
199                .join("wasm")
200                .join("release")
201                .join("build")
202                .join("ffi")
203                .join("ffi.core"),
204            dependencies: vec![],
205            package_sources: vec![(
206                format!("{moonbit_root_package}/ffi"),
207                Utf8Path::new("ffi").to_path_buf(),
208            )],
209        });
210        let ffi_dep = (
211            Utf8Path::new("target")
212                .join("wasm")
213                .join("release")
214                .join("build")
215                .join("ffi")
216                .join("ffi.mi"),
217            "ffi".to_string(),
218        );
219        gen_dependencies.push(ffi_dep.clone());
220
221        self.define_package(MoonBitPackage {
222            name: format!("{moonbit_root_package}/gen/world/{world_name}"),
223            mbt_files: vec![
224                Utf8Path::new("gen")
225                    .join("world")
226                    .join(&world_name)
227                    .join("stub.mbt"),
228            ],
229            warning_control: vec![],
230            alert_control: vec![],
231            output: Utf8Path::new("target")
232                .join("wasm")
233                .join("release")
234                .join("build")
235                .join("gen")
236                .join("world")
237                .join(&world_name)
238                .join(format!("{world_name}.core")),
239
240            dependencies: vec![],
241            package_sources: vec![(
242                format!("{moonbit_root_package}/gen/world/{world_name}"),
243                Utf8Path::new("gen").join("world").join(&world_name),
244            )],
245        });
246        gen_dependencies.push((
247            Utf8Path::new("target")
248                .join("wasm")
249                .join("release")
250                .join("build")
251                .join("gen")
252                .join("world")
253                .join(&world_name)
254                .join(format!("{world_name}.mi")),
255            world_name.clone(),
256        ));
257        gen_mbt_files.push(Utf8Path::new("gen").join(format!("world_{world_snake}_export.mbt")));
258
259        for (package_name, interface_name) in &imported_interfaces {
260            let pkg_namespace = to_moonbit_ident(&package_name.namespace);
261            let pkg_name = to_moonbit_ident(&package_name.name);
262            let interface_name = interface_name.to_lower_camel_case();
263
264            let name = format!(
265                "{moonbit_root_package}/interface/{pkg_namespace}/{pkg_name}/{interface_name}"
266            );
267            let src = Utf8Path::new("interface")
268                .join(&pkg_namespace)
269                .join(&pkg_name)
270                .join(&interface_name);
271            let output = Utf8Path::new("target")
272                .join("wasm")
273                .join("release")
274                .join("build")
275                .join("interface")
276                .join(&pkg_namespace)
277                .join(&pkg_name)
278                .join(&interface_name);
279            self.define_package(MoonBitPackage {
280                name: name.clone(),
281                mbt_files: vec![src.join("top.mbt"), src.join("ffi.mbt")],
282                warning_control: vec![],
283                alert_control: vec![],
284                output: output.join(format!("{interface_name}.core")),
285                dependencies: vec![ffi_dep.clone()],
286                package_sources: vec![(name, src)],
287            });
288        }
289
290        for (package_name, interface_name) in &exported_interfaces {
291            let pkg_namespace = to_moonbit_ident(&package_name.namespace);
292            let unescaped_pkg_namespace = package_name.namespace.to_snake_case();
293            let pkg_name = to_moonbit_ident(&package_name.name);
294            let unescaped_pkg_name = package_name.name.to_snake_case();
295            let interface_name = interface_name.to_lower_camel_case();
296            let snake_interface_name = interface_name.to_snake_case();
297
298            let name = format!(
299                "{moonbit_root_package}/gen/interface/{pkg_namespace}/{pkg_name}/{interface_name}"
300            );
301            let src = Utf8Path::new("gen")
302                .join("interface")
303                .join(&pkg_namespace)
304                .join(&pkg_name)
305                .join(&interface_name);
306            let output = Utf8Path::new("target")
307                .join("wasm")
308                .join("release")
309                .join("build")
310                .join("gen")
311                .join("interface")
312                .join(&pkg_namespace)
313                .join(&pkg_name)
314                .join(&interface_name);
315            self.define_package(MoonBitPackage {
316                name: name.clone(),
317                mbt_files: vec![src.join("top.mbt"), src.join("stub.mbt")],
318                warning_control: vec![],
319                alert_control: vec![],
320                output: output.join(format!("{interface_name}.core")),
321
322                dependencies: vec![],
323                package_sources: vec![(name, src)],
324            });
325            gen_dependencies.push((
326                output.join(format!("{interface_name}.mi")),
327                interface_name.clone(),
328            ));
329            gen_mbt_files.push(Utf8Path::new("gen").join(format!(
330                "gen_interface_{unescaped_pkg_namespace}_{unescaped_pkg_name}_{snake_interface_name}_export.mbt"
331            )));
332        }
333
334        gen_mbt_files.push(Utf8Path::new("gen").join("ffi.mbt"));
335        self.define_package(MoonBitPackage {
336            name: format!("{moonbit_root_package}/gen"),
337            mbt_files: gen_mbt_files,
338            warning_control: vec![],
339            alert_control: vec![],
340            output: Utf8Path::new("target")
341                .join("wasm")
342                .join("release")
343                .join("build")
344                .join("gen")
345                .join("gen.core"),
346            dependencies: gen_dependencies,
347            package_sources: vec![(
348                format!("{moonbit_root_package}/gen"),
349                Utf8Path::new("gen").to_path_buf(),
350            )],
351        });
352
353        Ok(())
354    }
355
356    pub fn set_warning_control(
357        &mut self,
358        package_name: &str,
359        warning_control: Vec<WarningControl>,
360    ) -> anyhow::Result<()> {
361        let package = self
362            .packages
363            .get_mut(package_name)
364            .ok_or_else(|| anyhow::anyhow!("Package '{package_name}' not found"))?;
365        package.warning_control = warning_control;
366        Ok(())
367    }
368
369    pub fn set_alert_control(
370        &mut self,
371        package_name: &str,
372        alert_control: Vec<WarningControl>,
373    ) -> anyhow::Result<()> {
374        let package = self
375            .packages
376            .get_mut(package_name)
377            .ok_or_else(|| anyhow::anyhow!("Package '{package_name}' not found"))?;
378        package.alert_control = alert_control;
379        Ok(())
380    }
381
382    /// Defines a custom MoonBit package
383    pub fn define_package(&mut self, package: MoonBitPackage) {
384        debug!("Adding MoonBit package: {}", package.name);
385        self.packages.insert(package.name.clone(), package);
386    }
387
388    /// Defines an additional dependency for a MoonBit package previously added with `define_package`.
389    pub fn add_dependency(
390        &mut self,
391        package_name: &str,
392        mi_path: &Utf8Path,
393        alias: &str,
394    ) -> anyhow::Result<()> {
395        debug!("Adding dependency: {package_name} ({mi_path}) as {alias}");
396        let package = self
397            .packages
398            .get_mut(package_name)
399            .ok_or_else(|| anyhow::anyhow!("Package '{package_name}' not found"))?;
400        package
401            .dependencies
402            .push((Utf8PathBuf::from(mi_path), alias.to_string()));
403        Ok(())
404    }
405
406    pub fn write_file(&self, relative_path: &Utf8Path, contents: &str) -> anyhow::Result<()> {
407        let path = self.dir.join(relative_path);
408        info!("Writing file: {path:?}");
409        if let Some(parent) = path.parent() {
410            std::fs::create_dir_all(parent).context("Creating directory for generated file")?;
411        }
412        std::fs::write(path, contents)?;
413        Ok(())
414    }
415
416    /// Writes the top level export stub for the selected world
417    pub fn write_world_stub(&self, moonbit_source: &str) -> anyhow::Result<()> {
418        let world_name = self.world_name()?;
419        let path = self
420            .dir
421            .join("gen")
422            .join("world")
423            .join(world_name)
424            .join("stub.mbt");
425        info!("Writing world stub to {path}");
426        std::fs::write(path, moonbit_source)?;
427        Ok(())
428    }
429
430    /// Writes the interface stub for a given package and interface name.
431    pub fn write_interface_stub(
432        &self,
433        package_name: &PackageName,
434        interface_name: &str,
435        moonbit_source: &str,
436    ) -> anyhow::Result<()> {
437        let package_name_snake = to_moonbit_ident(&package_name.name);
438        let package_namespace_snake = to_moonbit_ident(&package_name.namespace);
439
440        let path = self
441            .dir
442            .join("gen")
443            .join("interface")
444            .join(package_namespace_snake)
445            .join(package_name_snake)
446            .join(interface_name.to_lower_camel_case())
447            .join("stub.mbt");
448        info!("Writing interface stub to {path}");
449        std::fs::create_dir_all(path.parent().unwrap())
450            .context("Creating directory for interface stub")?;
451        std::fs::write(path, moonbit_source).context("Writing interface stub")?;
452        Ok(())
453    }
454
455    /// Writes the MoonBit package JSON file for a given exported package and interface name.
456    pub fn write_interface_package_json(
457        &self,
458        package_name: &PackageName,
459        interface_name: &str,
460        json: serde_json::Value,
461    ) -> anyhow::Result<()> {
462        let package_name_snake = to_moonbit_ident(&package_name.name);
463        let package_namespace_snake = to_moonbit_ident(&package_name.namespace);
464        let path = self
465            .dir
466            .join("gen")
467            .join("interface")
468            .join(package_namespace_snake)
469            .join(package_name_snake)
470            .join(interface_name.to_lower_camel_case())
471            .join("moon.pkg.json");
472        info!("Writing interface definition to {path}");
473        std::fs::create_dir_all(path.parent().unwrap())
474            .context("Creating directory for interface definition")?;
475        std::fs::write(
476            path,
477            serde_json::to_string_pretty(&json).context("Writing interface definition")?,
478        )?;
479        Ok(())
480    }
481
482    /// Builds the MoonBit component, compiling all packages and linking them together into a
483    /// final WASM component.
484    ///
485    /// The `main_package_name` is optional, if it is not provided, the binding generator's `gen` package will be used as the main package.
486    pub fn build(&self, main_package_name: Option<&str>, target: &Utf8Path) -> anyhow::Result<()> {
487        let main_package_name = match main_package_name {
488            Some(name) => name.to_string(),
489            None => {
490                let root_package = self.moonbit_root_package()?;
491                format!("{root_package}/gen")
492            }
493        };
494
495        let sorted_packages = self.sorted_packages()?;
496        debug!(
497            "Package build order: {}",
498            sorted_packages
499                .iter()
500                .map(|p| p.name.clone())
501                .collect::<Vec<_>>()
502                .join(", ")
503        );
504
505        for package in &sorted_packages {
506            self.build_package(
507                &package.mbt_files,
508                &package.warning_control,
509                &package.alert_control,
510                &package.output,
511                &package.name,
512                &package.dependencies,
513                &package.package_sources,
514            )
515            .context(format!("Building package {}", package.name))?;
516        }
517
518        let mut core_files = vec![
519            self.core_bundle_dir().join("abort").join("abort.core"),
520            self.core_bundle_dir().join("core.core"),
521        ];
522        let mut package_sources = BTreeMap::new();
523
524        for package in &sorted_packages {
525            core_files.push(package.output.clone());
526            for (name, source) in &package.package_sources {
527                package_sources.insert(name.clone(), source.clone());
528            }
529        }
530        package_sources.insert("moonbitlang/core".to_string(), self.core_dir());
531        let package_sources: Vec<(String, Utf8PathBuf)> = package_sources.into_iter().collect();
532
533        let main_package = self
534            .packages
535            .get(&main_package_name)
536            .ok_or_else(|| anyhow!(format!("Main package '{main_package_name}' not found")))?;
537        let (_, main_package_source) = main_package
538            .package_sources
539            .iter()
540            .find(|(name, _)| name == &main_package_name)
541            .ok_or_else(|| {
542                anyhow!(format!(
543                    "Main package sources '{main_package_name}' not found"
544                ))
545            })?;
546
547        let main_package_json = self.dir.join(main_package_source).join("moon.pkg.json");
548        let linker_config = Self::extract_wasm_linker_config(&main_package_json)
549            .context("Extracting linker config")?;
550
551        self.link_core(
552            &core_files,
553            &main_package_name,
554            &main_package_json,
555            &package_sources,
556            &linker_config.export_memory_name,
557            &linker_config.exports,
558            linker_config.heap_start_address,
559        )
560        .context("Linking")?;
561
562        self.embed_wit().context("Embedding WIT")?;
563        self.create_component(target)
564            .context("Creating component")?;
565
566        Ok(())
567    }
568
569    fn extract_core(&self) -> anyhow::Result<()> {
570        let core_dir = self.core_dir();
571        info!("Extracting MoonBit core to {core_dir}");
572        std::fs::create_dir_all(&core_dir)?;
573        MOONBIT_CORE.extract(&core_dir)?;
574        Ok(())
575    }
576
577    #[allow(clippy::too_many_arguments)]
578    fn build_package(
579        &self,
580        mbt_files: &[Utf8PathBuf],
581        warning_control: &[WarningControl],
582        alert_control: &[WarningControl],
583        output: &Utf8Path,
584        package: &str,
585        dependencies: &[(Utf8PathBuf, String)],
586        package_sources: &[(String, Utf8PathBuf)],
587    ) -> anyhow::Result<()> {
588        info!("Building MoonBit package: {package}");
589
590        for mbt_file in mbt_files {
591            let abs = self.dir.join(mbt_file);
592            if !abs.exists() {
593                return Err(anyhow!("MBT file does not exist at {abs}"));
594            }
595        }
596        for (path, name) in dependencies {
597            let abs = self.dir.join(path);
598            if !abs.exists() {
599                return Err(anyhow!("Dependency {name} does not exist at {abs}"));
600            }
601        }
602        for (source_name, source_path) in package_sources {
603            let abs = self.dir.join(source_path);
604            if !abs.exists() {
605                return Err(anyhow!(
606                    "Package source {source_name} does not exist at {abs}"
607                ));
608            }
609        }
610
611        let mut args = vec!["build-package".to_string()];
612        for file in mbt_files {
613            let full_path = self.dir.join(file);
614            args.push(full_path.to_string());
615        }
616        for w in warning_control {
617            args.push("-w".to_string());
618            args.push(w.to_string());
619        }
620        for a in alert_control {
621            args.push("-alert".to_string());
622            args.push(a.to_string());
623        }
624        args.push("-o".to_string());
625        args.push(self.dir.join(output).to_string());
626        args.push("-pkg".to_string());
627        args.push(package.to_string());
628        args.push("-std-path".to_string());
629        args.push(self.core_bundle_dir().to_string());
630        for (dep_path, dep_name) in dependencies {
631            args.push("-i".to_string());
632            let full_path = self.dir.join(dep_path);
633            args.push(format!("{full_path}:{dep_name}"));
634        }
635        self.add_package_sources(&mut args, package_sources);
636        args.push("-target".to_string());
637        args.push("wasm".to_string());
638
639        MOONC.run(args)?;
640
641        let abs = self.dir.join(output);
642        if !abs.exists() {
643            return Err(anyhow!("Output does not exist at {abs}"));
644        }
645
646        Ok(())
647    }
648
649    #[allow(clippy::too_many_arguments)]
650    fn link_core(
651        &self,
652        core_files: &[Utf8PathBuf],
653        main_package_name: &str,
654        main_package_json: &Utf8Path,
655        package_sources: &[(String, Utf8PathBuf)],
656        exported_memory_name: &str,
657        exported_functions: &[String],
658        heap_start_address: usize,
659    ) -> anyhow::Result<()> {
660        info!("Linking MoonBit component");
661        let mut args = vec!["link-core".to_string()];
662
663        for file in core_files {
664            let full_path = self.dir.join(file);
665            args.push(full_path.to_string());
666        }
667        args.push("-main".to_string());
668        args.push(main_package_name.to_string());
669        args.push("-o".to_string());
670        args.push(self.module_wasm().to_string());
671        args.push("-pkg-config-path".to_string());
672        args.push(self.dir.join(main_package_json).to_string());
673        self.add_package_sources(&mut args, package_sources);
674        args.push("-pkg-sources".to_string());
675        args.push(format!("moonbitlang/core:{}", self.core_dir()));
676        args.push("-target".to_string());
677        args.push("wasm".to_string());
678        args.push(format!(
679            "-exported_functions={}",
680            exported_functions.join(",")
681        ));
682        args.push("-export-memory-name".to_string());
683        args.push(exported_memory_name.to_string());
684        args.push("-heap-start-address".to_string());
685        args.push(heap_start_address.to_string());
686
687        MOONC.run(args)?;
688        Ok(())
689    }
690
691    fn add_package_sources(
692        &self,
693        args: &mut Vec<String>,
694        package_sources: &[(String, Utf8PathBuf)],
695    ) {
696        for (source_name, source_path) in package_sources {
697            args.push("-pkg-sources".to_string());
698            let full_path = self.dir.join(source_path);
699            args.push(format!("{source_name}:{full_path}"));
700        }
701    }
702
703    fn embed_wit(&self) -> anyhow::Result<()> {
704        info!("Embedding WIT in the compiled MoonBit WASM module");
705
706        // based on 'wasm-tools component embed'
707        let resolve = self.resolve.as_ref().unwrap();
708        let world = &self.world_id.unwrap();
709
710        let module_wasm = self.module_wasm();
711        let mut wasm = std::fs::read(&module_wasm)
712            .context(format!("Failed to read module WASM from {module_wasm}"))?;
713
714        wit_component::embed_component_metadata(&mut wasm, resolve, *world, StringEncoding::UTF16)
715            .context("Embedding component metadata")?;
716
717        std::fs::write(self.module_with_embed_wasm(), wasm)
718            .context("Writing WASM with embedded metadata")?;
719
720        Ok(())
721    }
722
723    fn create_component(&self, target: &Utf8Path) -> anyhow::Result<()> {
724        info!("Creating the final WASM component at {target}");
725
726        let wasm = std::fs::read(self.module_with_embed_wasm())
727            .context("Reading WASM with embedded metadata")?;
728        let mut encoder = ComponentEncoder::default()
729            .validate(true)
730            .reject_legacy_names(false)
731            .merge_imports_based_on_semver(true)
732            .realloc_via_memory_grow(false)
733            .module(&wasm)?;
734
735        let component = encoder.encode().context("Encoding WASM component")?;
736
737        if let Some(parent) = target.parent() {
738            std::fs::create_dir_all(parent).context("Creating directory for WASM component")?;
739        }
740        std::fs::write(target, component).context("Writing WASM component")?;
741
742        Ok(())
743    }
744
745    fn sorted_packages(&self) -> anyhow::Result<Vec<&MoonBitPackage>> {
746        let mut graph = AcyclicDependencyGraph::new();
747        let root_package = self.moonbit_root_package()?;
748
749        for package in self.packages.values() {
750            for (path, dep) in &package.dependencies {
751                let path_components = path.components().map(|c| c.to_string()).collect::<Vec<_>>();
752                let full_dep = if path_components.starts_with(&[
753                    "target".to_string(),
754                    "wasm".to_string(),
755                    "release".to_string(),
756                    "build".to_string(),
757                ]) {
758                    let relevant_path = &path_components[4..path_components.len() - 1];
759                    format!("{}/{}", root_package, relevant_path.join("/"))
760                } else {
761                    format!("{root_package}/{dep}")
762                };
763
764                graph.depend_on(package.name.clone(), full_dep)?;
765            }
766        }
767
768        let mut sorted = Vec::new();
769        let mut names = HashSet::new();
770        for layer in graph.get_forward_dependency_topological_layers() {
771            for package_name in layer {
772                sorted.push(&self.packages[&package_name]);
773                names.insert(package_name);
774            }
775        }
776
777        for package in self.packages.values() {
778            if !names.contains(&package.name) {
779                sorted.push(package);
780            }
781        }
782
783        Ok(sorted)
784    }
785
786    fn wit_dir(&self) -> Utf8PathBuf {
787        self.dir.join("wit")
788    }
789
790    fn core_dir(&self) -> Utf8PathBuf {
791        self.dir.join("core")
792    }
793
794    fn core_bundle_dir(&self) -> Utf8PathBuf {
795        self.dir
796            .join("core")
797            .join("target")
798            .join("wasm")
799            .join("release")
800            .join("bundle")
801    }
802
803    fn module_wasm(&self) -> Utf8PathBuf {
804        self.dir.join("target").join("module.wasm")
805    }
806
807    fn module_with_embed_wasm(&self) -> Utf8PathBuf {
808        self.dir.join("target").join("module.embed.wasm")
809    }
810
811    fn extract_wasm_linker_config(
812        package_json_path: &Utf8Path,
813    ) -> anyhow::Result<WasmLinkerConfig> {
814        debug!("Extracting Wasm linker config from {package_json_path}");
815        let json_str = std::fs::read_to_string(package_json_path)?;
816        let pkg: PackageJsonWithWasmLinkerConfig = serde_json::from_str(&json_str)?;
817
818        Ok(pkg.link.wasm)
819    }
820
821    pub fn moonbit_root_package(&self) -> anyhow::Result<String> {
822        Ok(format!(
823            "{}/{}",
824            self.root_pkg_namespace()?,
825            self.root_pkg_name()?
826        ))
827    }
828
829    pub fn root_pkg_namespace(&self) -> anyhow::Result<String> {
830        let root_package_id = self.root_package_id.as_ref().unwrap();
831        let resolve = self.resolve.as_ref().unwrap();
832
833        let root_package = resolve
834            .packages
835            .get(*root_package_id)
836            .ok_or_else(|| anyhow!("Root package not found"))?;
837        Ok(root_package.name.namespace.to_string())
838    }
839
840    pub fn root_pkg_name(&self) -> anyhow::Result<String> {
841        let root_package_id = self.root_package_id.as_ref().unwrap();
842        let resolve = self.resolve.as_ref().unwrap();
843
844        let root_package = resolve
845            .packages
846            .get(*root_package_id)
847            .ok_or_else(|| anyhow!("Root package not found"))?;
848        Ok(root_package.name.name.to_string())
849    }
850
851    fn world_name(&self) -> anyhow::Result<String> {
852        Ok(self
853            .resolve
854            .as_ref()
855            .and_then(|r| r.worlds.get(self.world_id?))
856            .map(|w| w.name.to_string())
857            .ok_or_else(|| anyhow::anyhow!("Could not find world"))?
858            .to_lower_camel_case())
859    }
860
861    fn get_imported_interfaces(&self) -> anyhow::Result<Vec<(PackageName, String)>> {
862        let world = self
863            .resolve
864            .as_ref()
865            .and_then(|r| r.worlds.get(self.world_id?))
866            .ok_or_else(|| anyhow::anyhow!("Could not find world"))?;
867        let mut imported_interfaces = Vec::new();
868        for (_, item) in &world.imports {
869            if let wit_parser::WorldItem::Interface { id, .. } = item
870                && let Some(interface) = self.resolve.as_ref().and_then(|r| r.interfaces.get(*id))
871            {
872                if let Some(interface_name) = interface.name.as_ref() {
873                    let owner_package = interface.package.ok_or_else(|| {
874                        anyhow::anyhow!("Interface '{}' does not have a package", interface_name)
875                    })?;
876                    let package = self
877                        .resolve
878                        .as_ref()
879                        .and_then(|r| r.packages.get(owner_package))
880                        .ok_or_else(|| {
881                            anyhow::anyhow!("Package for interface '{}' not found", interface_name)
882                        })?;
883
884                    imported_interfaces.push((package.name.clone(), interface_name.to_string()));
885                } else {
886                    return Err(anyhow::anyhow!(
887                        "Anonymous imported interfaces are not supported"
888                    ));
889                }
890            }
891        }
892        Ok(imported_interfaces)
893    }
894
895    fn get_exported_interfaces(&self) -> anyhow::Result<Vec<(PackageName, String)>> {
896        let world = self
897            .resolve
898            .as_ref()
899            .and_then(|r| r.worlds.get(self.world_id?))
900            .ok_or_else(|| anyhow::anyhow!("Could not find world"))?;
901        let mut exported_interfaces = Vec::new();
902        for (_, item) in &world.exports {
903            if let wit_parser::WorldItem::Interface { id, .. } = item
904                && let Some(interface) = self.resolve.as_ref().and_then(|r| r.interfaces.get(*id))
905            {
906                if let Some(interface_name) = interface.name.as_ref() {
907                    let owner_package = interface.package.ok_or_else(|| {
908                        anyhow::anyhow!("Interface '{}' does not have a package", interface_name)
909                    })?;
910                    let package = self
911                        .resolve
912                        .as_ref()
913                        .and_then(|r| r.packages.get(owner_package))
914                        .ok_or_else(|| {
915                            anyhow::anyhow!("Package for interface '{}' not found", interface_name)
916                        })?;
917
918                    exported_interfaces.push((package.name.clone(), interface_name.to_string()));
919                } else {
920                    return Err(anyhow::anyhow!(
921                        "Anonymous exported interfaces are not supported"
922                    ));
923                }
924            }
925        }
926        Ok(exported_interfaces)
927    }
928}
929
930#[derive(Debug, Deserialize)]
931struct PackageJsonWithWasmLinkerConfig {
932    link: LinkConfig,
933}
934
935#[derive(Debug, Deserialize)]
936struct LinkConfig {
937    wasm: WasmLinkerConfig,
938}
939
940#[derive(Debug, Deserialize)]
941#[serde(rename_all = "kebab-case")]
942struct WasmLinkerConfig {
943    export_memory_name: String,
944    exports: Vec<String>,
945    heap_start_address: usize,
946}
947
948pub struct MoonBitPackage {
949    pub name: String,
950    pub mbt_files: Vec<Utf8PathBuf>,
951    pub warning_control: Vec<WarningControl>,
952    pub alert_control: Vec<WarningControl>,
953    pub output: Utf8PathBuf,
954    pub dependencies: Vec<(Utf8PathBuf, String)>,
955    pub package_sources: Vec<(String, Utf8PathBuf)>,
956}
957
958pub enum Warning {
959    Specific(u16),
960    Range(Range<u16>),
961}
962
963impl Display for Warning {
964    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
965        match self {
966            Warning::Specific(code) => write!(f, "{code}"),
967            Warning::Range(range) => write!(f, "{}..{}", range.start, range.end),
968        }
969    }
970}
971
972pub enum WarningControl {
973    Enable(Warning),
974    Disable(Warning),
975    EnableAsError(Warning),
976}
977
978impl Display for WarningControl {
979    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
980        match self {
981            WarningControl::Enable(code) => write!(f, "+{code}"),
982            WarningControl::Disable(code) => write!(f, "-{code}"),
983            WarningControl::EnableAsError(code) => write!(f, "@{code}"),
984        }
985    }
986}
987
988pub fn to_moonbit_ident(name: impl AsRef<str>) -> String {
989    // Escape MoonBit keywords and reserved keywords
990    let name = name.as_ref();
991    match name {
992        // Keywords
993        "as" | "else" | "extern" | "fn" | "fnalias" | "if" | "let" | "const" | "match" | "using"
994        | "mut" | "type" | "typealias" | "struct" | "enum" | "trait" | "traitalias" | "derive"
995        | "while" | "break" | "continue" | "import" | "return" | "throw" | "raise" | "try" | "catch"
996        | "pub" | "priv" | "readonly" | "true" | "false" | "_" | "test" | "loop" | "for" | "in" | "impl"
997        | "with" | "guard" | "async" | "is" | "suberror" | "and" | "letrec" | "enumview" | "noraise"
998        | "defer" | "init" | "main"
999        // Reserved keywords
1000        | "module" | "move" | "ref" | "static" | "super" | "unsafe" | "use" | "where" | "await"
1001        | "dyn" | "abstract" | "do" | "final" | "macro" | "override" | "typeof" | "virtual" | "yield"
1002        | "local" | "method" | "alias" | "assert" | "recur" | "isnot" | "define" | "downcast"
1003        | "inherit" | "member" | "namespace" | "upcast" | "void" | "lazy" | "include" | "mixin"
1004        | "protected" | "sealed" | "constructor" | "atomic" | "volatile" | "anyframe" | "anytype"
1005        | "asm" | "comptime" | "errdefer" | "export" | "opaque" | "orelse" | "resume" | "threadlocal"
1006        | "unreachable" | "dynclass" | "dynobj" | "dynrec" | "var" | "finally" | "noasync" => {
1007            format ! ("{name}_")
1008        }
1009        _ => name.to_snake_case(),
1010    }
1011}
1012
1013#[cfg(test)]
1014test_r::enable!();
1015
1016#[cfg(test)]
1017struct Trace;
1018
1019#[cfg(test)]
1020#[test_r::test_dep]
1021fn initialize_trace() -> Trace {
1022    pretty_env_logger::formatted_builder()
1023        .filter_level(log::LevelFilter::Debug)
1024        .write_style(pretty_env_logger::env_logger::WriteStyle::Always)
1025        .init();
1026    Trace
1027}