Skip to main content

formawasm/
component.rs

1//! Component-Model wrap layer.
2//!
3//! [`wrap_component`] takes the core-Wasm bytes produced by
4//! [`crate::lower_module`] together with the WIT source produced by
5//! [`crate::emit_wit`], embeds the corresponding world identity into
6//! the core module's `component-type` custom section, and runs
7//! [`wit_component::ComponentEncoder`] to emit a final Component-Model
8//! artifact.
9//!
10//! The WIT must declare exactly one world named [`crate::wit::WORLD_NAME`]
11//! — that's the contract `emit_wit` produces and `wrap_component`
12//! consumes.
13
14use thiserror::Error;
15use wit_component::{ComponentEncoder, StringEncoding, embed_component_metadata};
16use wit_parser::Resolve;
17
18use crate::wit::WORLD_NAME;
19
20/// Errors produced by [`wrap_component`].
21#[derive(Debug, Error)]
22#[non_exhaustive]
23pub enum ComponentWrapError {
24    /// The supplied WIT string failed to parse via `wit-parser`.
25    /// Should not happen for strings produced by [`crate::emit_wit`],
26    /// which round-trips its own output before returning.
27    #[error("WIT failed to parse: {reason}")]
28    WitParse {
29        /// `wit-parser`'s diagnostic rendered as a string.
30        reason: String,
31    },
32
33    /// The supplied WIT did not declare a world named
34    /// [`crate::wit::WORLD_NAME`].
35    #[error("WIT does not declare a world named '{WORLD_NAME}': {reason}")]
36    WorldMissing {
37        /// `wit-parser`'s diagnostic rendered as a string.
38        reason: String,
39    },
40
41    /// `wit_component::embed_component_metadata` failed to attach the
42    /// custom section to the core module.
43    #[error("failed to embed component-type metadata into core module: {reason}")]
44    EmbedFailed {
45        /// `wit-component`'s diagnostic rendered as a string.
46        reason: String,
47    },
48
49    /// `ComponentEncoder::module` rejected the core-Wasm bytes — most
50    /// likely because the embedded metadata refers to exports the
51    /// module does not actually provide.
52    #[error("ComponentEncoder rejected the core module: {reason}")]
53    EncoderRejected {
54        /// `wit-component`'s diagnostic rendered as a string.
55        reason: String,
56    },
57
58    /// `ComponentEncoder::encode` produced no bytes — the encoder ran
59    /// to completion but the result failed component-model validation.
60    #[error("ComponentEncoder failed to encode the component: {reason}")]
61    EncodeFailed {
62        /// `wit-component`'s diagnostic rendered as a string.
63        reason: String,
64    },
65}
66
67/// Wrap `core_bytes` into a Component-Model artifact.
68///
69/// `core_bytes` is consumed because `wit_component::embed_component_metadata`
70/// appends the custom section in place. `wit` must declare exactly
71/// one world named [`crate::wit::WORLD_NAME`].
72pub fn wrap_component(mut core_bytes: Vec<u8>, wit: &str) -> Result<Vec<u8>, ComponentWrapError> {
73    let mut resolve = Resolve::default();
74    let pkg = resolve
75        .push_str("formawasm-generated.wit", wit)
76        .map_err(|e| ComponentWrapError::WitParse {
77            reason: format!("{e:#}"),
78        })?;
79    let world = resolve
80        .select_world(&[pkg], Some(WORLD_NAME))
81        .map_err(|e| ComponentWrapError::WorldMissing {
82            reason: format!("{e:#}"),
83        })?;
84
85    embed_component_metadata(&mut core_bytes, &resolve, world, StringEncoding::UTF8).map_err(
86        |e| ComponentWrapError::EmbedFailed {
87            reason: format!("{e:#}"),
88        },
89    )?;
90
91    let mut encoder = ComponentEncoder::default()
92        .validate(true)
93        .module(&core_bytes)
94        .map_err(|e| ComponentWrapError::EncoderRejected {
95            reason: format!("{e:#}"),
96        })?;
97    encoder
98        .encode()
99        .map_err(|e| ComponentWrapError::EncodeFailed {
100            reason: format!("{e:#}"),
101        })
102}