Skip to main content

perry_wit/component/
wit.rs

1//! Versioned WASI and custom WIT package resolution.
2
3use std::collections::HashSet;
4use std::fmt;
5use std::path::{Path, PathBuf};
6
7use anyhow::{Context, Result};
8use wit_parser::{PackageId, Resolve, SourceMap, UnresolvedPackageGroup};
9
10#[derive(Debug)]
11enum WitError {
12    MissingDirectory {
13        dir: PathBuf,
14    },
15    ParsePackage {
16        dir: PathBuf,
17        missing_package_header: bool,
18        source: anyhow::Error,
19    },
20    ResolveDependencies {
21        target: String,
22        source: anyhow::Error,
23    },
24    SelectWorld {
25        package_name: String,
26        has_requested_world: bool,
27        available: Vec<String>,
28        source: anyhow::Error,
29    },
30    EmptySources,
31    InvalidPackageEncoding,
32    Other(anyhow::Error),
33}
34
35impl WitError {
36    fn help(&self) -> Option<String> {
37        match self {
38            Self::MissingDirectory { dir } => Some(format!(
39                "Create '{}/world.wit' or pass '--wit <PATH>' to specify your WIT directory.",
40                dir.display()
41            )),
42            Self::ParsePackage {
43                missing_package_header: true,
44                ..
45            } => Some(
46                "Add a package header such as 'package local:app;' at the top of your .wit file, or ensure the directory contains at least one .wit file.".to_owned(),
47            ),
48            Self::SelectWorld {
49                package_name,
50                has_requested_world,
51                available,
52                ..
53            } => Some(match (available.is_empty(), *has_requested_world) {
54                (true, _) => {
55                    "Declare a 'world <name> { ... }' block in your WIT package.".to_owned()
56                }
57                (false, true) => {
58                    format!(
59                        "Available worlds in package '{package_name}': {}.",
60                        available.join(", ")
61                    )
62                }
63                (false, false) => {
64                    format!(
65                        "Pass '--world <NAME>' to select a world (available: {}).",
66                        available.join(", ")
67                    )
68                }
69            }),
70            _ => None,
71        }
72    }
73}
74
75impl fmt::Display for WitError {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        match self {
78            Self::MissingDirectory { dir } => {
79                write!(f, "WIT directory '{}' does not exist", dir.display())
80            }
81            Self::ParsePackage { dir, .. } => {
82                write!(f, "loading WIT package from {}", dir.display())
83            }
84            Self::ResolveDependencies { target, .. } => {
85                write!(f, "resolving WIT dependencies for {target}")
86            }
87            Self::SelectWorld { source, .. } | Self::Other(source) => write!(f, "{source}"),
88            Self::EmptySources => f.write_str("WIT package has no sources"),
89            Self::InvalidPackageEncoding => {
90                f.write_str("expected an encoded WIT package, not an application component")
91            }
92        }
93    }
94}
95
96impl std::error::Error for WitError {
97    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
98        match self {
99            Self::ParsePackage { source, .. } | Self::ResolveDependencies { source, .. } => {
100                Some(source.as_ref())
101            }
102            Self::SelectWorld { source, .. } | Self::Other(source) => source.source(),
103            Self::MissingDirectory { .. } | Self::EmptySources | Self::InvalidPackageEncoding => {
104                None
105            }
106        }
107    }
108}
109
110impl From<WitError> for crate::PerryDiagnostic {
111    fn from(error: WitError) -> Self {
112        let help = error.help();
113        let mut diagnostic = Self::error(crate::DiagnosticCode::WitResolution, error.to_string());
114        if let Some(help) = help {
115            diagnostic = diagnostic.with_help(help);
116        }
117        if let Some(source) = std::error::Error::source(&error) {
118            diagnostic = diagnostic.with_source_notes(source);
119        }
120        diagnostic
121    }
122}
123
124mod embedded {
125    include!(concat!(env!("OUT_DIR"), "/wasi_packages.rs"));
126}
127
128fn source_package(sources: &[crate::api::Source]) -> Result<UnresolvedPackageGroup> {
129    if sources.is_empty() {
130        return Err(crate::PerryDiagnostic::from(WitError::EmptySources).into());
131    }
132    let mut map = SourceMap::new();
133    for (index, source) in sources.iter().enumerate() {
134        let text = crate::api::text(&source.contents, source.name.as_deref())
135            .map_err(|e| crate::PerryDiagnostic::from(WitError::Other(e)))?;
136        match source.name.as_deref() {
137            Some(name) => map.push_str(name, text),
138            None => map.push_str(&format!("source-{index}.wit"), text),
139        }
140    }
141    map.parse().map_err(|(map, error)| {
142        crate::PerryDiagnostic::from(WitError::Other(anyhow::anyhow!(error.render(&map)))).into()
143    })
144}
145
146/// Resolve only supplied package contents and the compiler's pinned standard packages.
147pub(crate) fn resolve_contents(input: &crate::api::Wit) -> Result<(Resolve, PackageId)> {
148    let mut resolve = Resolve::new();
149    let mut groups = Vec::new();
150    let mut application = None;
151    for (index, package) in std::iter::once(&input.application)
152        .chain(&input.dependencies)
153        .enumerate()
154    {
155        match package {
156            crate::api::Package::Sources(sources) => groups.push(source_package(sources)?),
157            crate::api::Package::Encoded(bytes) => {
158                let decoded = wit_parser::decoding::decode(bytes)
159                    .map_err(|e| crate::PerryDiagnostic::from(WitError::Other(e)))?;
160                let wit_parser::decoding::DecodedWasm::WitPackage(decoded, package) = decoded
161                else {
162                    return Err(
163                        crate::PerryDiagnostic::from(WitError::InvalidPackageEncoding).into(),
164                    );
165                };
166                let remap = resolve
167                    .merge(decoded)
168                    .map_err(|e| crate::PerryDiagnostic::from(WitError::Other(e)))?;
169                if index == 0 {
170                    application = Some(remap.packages[package.index()]);
171                }
172            }
173        }
174    }
175    let main = if application.is_none() {
176        groups.remove(0)
177    } else {
178        // push_groups needs a main package; the marker only anchors explicit dependencies.
179        UnresolvedPackageGroup::parse("dependencies.wit", "package perry:compiler-dependencies;")
180            .map_err(|(map, error)| {
181                crate::PerryDiagnostic::from(WitError::Other(anyhow::anyhow!(error.render(&map))))
182            })?
183    };
184    let package = resolve_with(main, &mut resolve, groups, false)
185        .map_err(|e| crate::PerryDiagnostic::from(WitError::Other(e)))?;
186    Ok((resolve, application.unwrap_or(package)))
187}
188
189pub(crate) fn resolve_embedded_source(name: &str, source: &str) -> Result<(Resolve, PackageId)> {
190    resolve_contents(&crate::api::Wit {
191        application: crate::api::Package::Sources(vec![crate::api::Source {
192            name: Some(name.into()),
193            contents: source.as_bytes().to_vec(),
194        }]),
195        dependencies: Vec::new(),
196    })
197}
198
199fn parse_dir(path: &Path) -> Result<UnresolvedPackageGroup> {
200    let mut map = SourceMap::default();
201    map.push_dir(path)?;
202    map.parse()
203        .map_err(|(map, error)| anyhow::anyhow!(error.render(&map)))
204}
205
206fn read_dependencies(dir: &Path, resolve: &mut Resolve) -> Result<Vec<UnresolvedPackageGroup>> {
207    if !dir.exists() {
208        return Ok(Vec::new());
209    }
210    let mut entries = std::fs::read_dir(dir)?.collect::<std::io::Result<Vec<_>>>()?;
211    entries.sort_by_key(|entry| entry.file_name());
212    let mut groups = Vec::new();
213    for entry in entries {
214        let path = entry.path();
215        let group = if path.is_dir() {
216            parse_dir(&path)?
217        } else {
218            match path.extension().and_then(|ext| ext.to_str()) {
219                Some("wit") => {
220                    let source = std::fs::read_to_string(&path)?;
221                    UnresolvedPackageGroup::parse(&path, &source)
222                        .map_err(|(map, error)| anyhow::anyhow!(error.render(&map)))?
223                }
224                Some("wasm" | "wat") => {
225                    resolve.push_file(&path)?;
226                    continue;
227                }
228                _ => continue,
229            }
230        };
231        groups.push(group);
232    }
233    Ok(groups)
234}
235
236pub fn resolve_wit(wit_dir: &Path) -> Result<(Resolve, PackageId)> {
237    if !wit_dir.exists() {
238        return Err(crate::PerryDiagnostic::from(WitError::MissingDirectory {
239            dir: wit_dir.to_path_buf(),
240        })
241        .into());
242    }
243    let mut resolve = Resolve::new();
244    let main = parse_dir(wit_dir).map_err(|source| {
245        let missing_package_header = source
246            .chain()
247            .any(|c| c.to_string().contains("no `package` header was found"));
248        crate::PerryDiagnostic::from(WitError::ParsePackage {
249            dir: wit_dir.to_path_buf(),
250            missing_package_header,
251            source,
252        })
253    })?;
254    let deps = read_dependencies(&wit_dir.join("deps"), &mut resolve)
255        .map_err(|e| crate::PerryDiagnostic::from(WitError::Other(e)))?;
256    resolve_with(main, &mut resolve, deps, true)
257        .map_err(|source| {
258            crate::PerryDiagnostic::from(WitError::ResolveDependencies {
259                target: wit_dir.display().to_string(),
260                source,
261            })
262            .into()
263        })
264        .map(|package| (resolve, package))
265}
266
267pub(crate) fn select_world(
268    resolve: &Resolve,
269    package: PackageId,
270    world: Option<&str>,
271) -> Result<wit_parser::WorldId> {
272    resolve.select_world(&[package], world).map_err(|source| {
273        let available = resolve.packages[package].worlds.keys().cloned().collect();
274        crate::PerryDiagnostic::from(WitError::SelectWorld {
275            package_name: resolve.packages[package].name.to_string(),
276            has_requested_world: world.is_some(),
277            available,
278            source,
279        })
280        .into()
281    })
282}
283
284/// Resolve a WIT package given as source, with the ambient WASI packages.
285pub fn resolve_source(name: &str, source: &str) -> Result<(Resolve, PackageId)> {
286    let mut resolve = Resolve::new();
287    let main = UnresolvedPackageGroup::parse(name, source)
288        .map_err(|(map, error)| anyhow::anyhow!(error.render(&map)))?;
289    resolve_with(main, &mut resolve, Vec::new(), true)
290        .with_context(|| format!("resolving WIT dependencies for {name}"))
291        .map(|package| (resolve, package))
292}
293
294fn resolve_with(
295    main: UnresolvedPackageGroup,
296    resolve: &mut Resolve,
297    mut deps: Vec<UnresolvedPackageGroup>,
298    ambient: bool,
299) -> Result<PackageId> {
300    let ambient_groups = match ambient.then(|| std::env::var_os("WASI_WIT_PATH")).flatten() {
301        Some(path) => read_dependencies(&PathBuf::from(path), resolve)?,
302        None => Vec::new(),
303    };
304    let mut available: HashSet<_> = resolve.package_names.keys().cloned().collect();
305    for group in std::iter::once(&main).chain(deps.iter()) {
306        available.insert(group.main.name.clone());
307        available.extend(group.nested.iter().map(|package| package.name.clone()));
308    }
309    let mut push_unique = |mut group: UnresolvedPackageGroup| {
310        if available.insert(group.main.name.clone()) {
311            group
312                .nested
313                .retain(|package| available.insert(package.name.clone()));
314            deps.push(group);
315        }
316    };
317    for group in ambient_groups {
318        push_unique(group);
319    }
320    for files in embedded::PACKAGES {
321        let mut map = SourceMap::new();
322        for (name, source) in *files {
323            map.push_str(name, *source);
324        }
325        push_unique(
326            map.parse()
327                .map_err(|(map, error)| anyhow::anyhow!(error.render(&map)))?,
328        );
329    }
330    Ok(resolve.push_groups(main, deps)?)
331}