1use 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
146pub(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 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
284pub 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}