Repository: bytecodealliance/wasm-tools
Pull request #1611 (2024-06-11 15:48:42)

Fold `resolve_world_from_name` helper into `select_world`
This is a follow-up to #1577 to refactor a bit to have bindings generators continue to use `Resolve::select_world` for figuring out what to generate bindings for.


Patch:
diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs
index 057e755c..56eecf06 100644
--- a/crates/wit-component/src/encoding.rs
+++ b/crates/wit-component/src/encoding.rs
@@ -2129,39 +2129,32 @@ impl ComponentEncoder {
 
 #[cfg(all(test, feature = "dummy-module"))]
 mod test {
-    use crate::{dummy_module, embed_component_metadata};
-
     use super::*;
-    use std::path::Path;
-    use wit_parser::UnresolvedPackageGroup;
+    use crate::{dummy_module, embed_component_metadata};
 
     #[test]
     fn it_renames_imports() {
         let mut resolve = Resolve::new();
-        let UnresolvedPackageGroup {
-            mut packages,
-            source_map,
-        } = UnresolvedPackageGroup::parse(
-            Path::new("test.wit"),
-            r#"
+        let pkgs = resolve
+            .push_str(
+                "test.wit",
+                r#"
 package test:wit;
 
 interface i {
-f: func();
+    f: func();
 }
 
 world test {
-import i;
-import foo: interface {
-f: func();
-}
+    import i;
+    import foo: interface {
+        f: func();
+    }
 }
 "#,
-        )
-        .unwrap();
-        let pkg = resolve.push(packages.remove(0), &source_map).unwrap();
-
-        let world = resolve.select_world(pkg, None).unwrap();
+            )
+            .unwrap();
+        let world = resolve.select_world(&pkgs, None).unwrap();
 
         let mut module = dummy_module(&resolve, world);
 
diff --git a/crates/wit-component/src/lib.rs b/crates/wit-component/src/lib.rs
index 7d81335e..ba175aa8 100644
--- a/crates/wit-component/src/lib.rs
+++ b/crates/wit-component/src/lib.rs
@@ -5,9 +5,9 @@
 use std::str::FromStr;
 use std::{borrow::Cow, fmt::Display};
 
-use anyhow::{bail, Context, Result};
+use anyhow::{bail, Result};
 use wasm_encoder::{CanonicalOption, Encode, Section};
-use wit_parser::{parse_use_path, PackageId, ParsedUsePath, Resolve, WorldId};
+use wit_parser::{Resolve, WorldId};
 
 mod encoding;
 mod gc;
@@ -79,43 +79,6 @@ impl From<StringEncoding> for wasm_encoder::CanonicalOption {
     }
 }
 
-/// Handles world name resolution for cases when multiple packages may have been resolved. If this
-/// is the case, and we're dealing with input that contains a user-supplied world name (like via a
-/// CLI command, for instance), we want to ensure that the world name follows the following rules:
-///
-///   * If there is a single resolved package with a single world, the world name name MAY be
-///     omitted.
-///   * If there is a single resolved package with multiple worlds, the world name MUST be supplied,
-///     but MAY or MAY NOT be fully-qualified.
-///   * If there are multiple resolved packages, the world name MUST be fully-qualified.
-pub fn resolve_world_from_name(
-    resolve: &Resolve,
-    resolved_packages: Vec<PackageId>,
-    world_name: Option<&str>,
-) -> Result<WorldId> {
-    match resolved_packages.len() {
-        0 => bail!("all of the supplied WIT source files were empty"),
-        1 => resolve.select_world(resolved_packages[0], world_name.as_deref()),
-        _ => match world_name.as_deref() {
-            Some(name) => {
-                let world_path = parse_use_path(name).with_context(|| {
-                    format!("failed to parse world specifier `{name}`")
-                })?;
-                match world_path {
-                        ParsedUsePath::Name(name) => bail!("the world specifier must be of the fully-qualified, id-based form (ex: \"wasi:http/proxy\" rather than \"proxy\"); you used {name}"),
-                        ParsedUsePath::Package(pkg_name, _) => {
-                            match resolve.package_names.get(&pkg_name) {
-                                Some(pkg_id) => resolve.select_world(pkg_id.clone(), world_name.as_deref()),
-                                None => bail!("the world specifier you provided named {pkg_name}, but no package with that name was found"),
-                            }
-                        }
-                    }
-            }
-            None => bail!("the supplied WIT source files describe multiple packages; please provide a fully-qualified world-specifier to the `embed` command"),
-        },
-    }
-}
-
 /// A producer section to be added to all modules and components synthesized by
 /// this crate
 pub(crate) fn base_producers() -> wasm_metadata::Producers {
@@ -145,11 +108,9 @@ pub fn embed_component_metadata(
 
 #[cfg(test)]
 mod tests {
-    use std::path::Path;
-
     use anyhow::Result;
     use wasmparser::Payload;
-    use wit_parser::{Resolve, UnresolvedPackageGroup};
+    use wit_parser::Resolve;
 
     use super::{embed_component_metadata, StringEncoding};
 
@@ -184,12 +145,8 @@ world test-world {}
 
         // Parse pre-canned WIT to build resolver
         let mut resolver = Resolve::default();
-        let UnresolvedPackageGroup {
-            mut packages,
-            source_map,
-        } = UnresolvedPackageGroup::parse(&Path::new("in-code.wit"), COMPONENT_WIT)?;
-        let pkg_id = resolver.push(packages.remove(0), &source_map)?;
-        let world = resolver.select_world(pkg_id, Some("test-world").into())?;
+        let pkgs = resolver.push_str("in-code.wit", COMPONENT_WIT)?;
+        let world = resolver.select_world(&pkgs, Some("test-world"))?;
 
         // Embed component metadata
         embed_component_metadata(&mut bytes, &resolver, world, StringEncoding::UTF8)?;
diff --git a/crates/wit-component/src/metadata.rs b/crates/wit-component/src/metadata.rs
index 59cb7353..f3e43614 100644
--- a/crates/wit-component/src/metadata.rs
+++ b/crates/wit-component/src/metadata.rs
@@ -42,7 +42,7 @@
 //! the three arguments originally passed to `encode`.
 
 use crate::validation::BARE_FUNC_MODULE_NAME;
-use crate::{resolve_world_from_name, DecodedWasm, StringEncoding};
+use crate::{DecodedWasm, StringEncoding};
 use anyhow::{bail, Context, Result};
 use indexmap::IndexMap;
 use std::borrow::Cow;
@@ -264,7 +264,7 @@ impl Bindgen {
                     DecodedWasm::Component(..) => bail!("expected encoded wit package(s)"),
                 };
                 resolve = r;
-                world = resolve_world_from_name(&resolve, pkgs, Some(world_name.into()))?;
+                world = resolve.select_world(&pkgs, Some(world_name.into()))?;
             }
 
             // Current format where `data` is a wasm component itself.
diff --git a/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs
index 72e0857b..2ab630cc 100644
--- a/crates/wit-parser/src/lib.rs
+++ b/crates/wit-parser/src/lib.rs
@@ -228,10 +228,11 @@ impl UnresolvedPackageGroup {
     /// Parses the given string as a wit document.
     ///
     /// The `path` argument is used for error reporting. The `contents` provided
-    /// will not be able to use `pkg` use paths to other documents.
-    pub fn parse(path: &Path, contents: &str) -> Result<UnresolvedPackageGroup> {
+    /// are considered to be the contents of `path`. This function does not read
+    /// the filesystem.
+    pub fn parse(path: impl AsRef<Path>, contents: &str) -> Result<UnresolvedPackageGroup> {
         let mut map = SourceMap::default();
-        map.push(path, contents);
+        map.push(path.as_ref(), contents);
         map.parse()
     }
 
@@ -240,7 +241,8 @@ impl UnresolvedPackageGroup {
     /// The path provided is inferred whether it's a file or a directory. A file
     /// is parsed with [`UnresolvedPackageGroup::parse_file`] and a directory is
     /// parsed with [`UnresolvedPackageGroup::parse_dir`].
-    pub fn parse_path(path: &Path) -> Result<UnresolvedPackageGroup> {
+    pub fn parse_path(path: impl AsRef<Path>) -> Result<UnresolvedPackageGroup> {
+        let path = path.as_ref();
         if path.is_dir() {
             UnresolvedPackageGroup::parse_dir(path)
         } else {
@@ -250,9 +252,10 @@ impl UnresolvedPackageGroup {
 
     /// Parses a WIT package from the file provided.
     ///
-    /// The WIT package returned will be a single-document package and will not
-    /// be able to use `pkg` paths to other documents.
-    pub fn parse_file(path: &Path) -> Result<UnresolvedPackageGroup> {
+    /// The return value represents all packages found in the WIT file which
+    /// might be either one or multiple depending on the syntax used.
+    pub fn parse_file(path: impl AsRef<Path>) -> Result<UnresolvedPackageGroup> {
+        let path = path.as_ref();
         let contents = std::fs::read_to_string(path)
             .with_context(|| format!("failed to read file {path:?}"))?;
         Self::parse(path, &contents)
@@ -260,9 +263,12 @@ impl UnresolvedPackageGroup {
 
     /// Parses a WIT package from the directory provided.
     ///
-    /// All files with the extension `*.wit` or `*.wit.md` will be loaded from
-    /// `path` into the returned package.
-    pub fn parse_dir(path: &Path) -> Result<UnresolvedPackageGroup> {
+    /// This method will look at all files under the `path` specified. All
+    /// `*.wit` files are parsed and assumed to be part of the same package
+    /// grouping. This is useful when a WIT package is split across multiple
+    /// files.
+    pub fn parse_dir(path: impl AsRef<Path>) -> Result<UnresolvedPackageGroup> {
+        let path = path.as_ref();
         let mut map = SourceMap::default();
         let cx = || format!("failed to read directory {path:?}");
         for entry in path.read_dir().with_context(&cx)? {
@@ -281,7 +287,7 @@ impl UnresolvedPackageGroup {
                 Some(name) => name,
                 None => continue,
             };
-            if !filename.ends_with(".wit") && !filename.ends_with(".wit.md") {
+            if !filename.ends_with(".wit") {
                 continue;
             }
             map.push_file(&path)?;
diff --git a/crates/wit-parser/src/resolve.rs b/crates/wit-parser/src/resolve.rs
index a41290a6..c5a2f237 100644
--- a/crates/wit-parser/src/resolve.rs
+++ b/crates/wit-parser/src/resolve.rs
@@ -154,23 +154,35 @@ impl Resolve {
         Resolve::default()
     }
 
-    /// Parse a WIT package from the input `path`.
+    /// Parse WIT packages from the input `path`.
     ///
     /// The input `path` can be one of:
     ///
     /// * A directory containing a WIT package with an optional `deps` directory
     ///   for any dependent WIT packages it references.
-    /// * A single standalone WIT file depending on what's already in `Resolve`.
+    /// * A single standalone WIT file.
     /// * A wasm-encoded WIT package as a single file in the wasm binary format.
     /// * A wasm-encoded WIT package as a single file in the wasm text format.
     ///
-    /// The `PackageId` of the parsed package is returned. For more information
-    /// see [`Resolve::push_dir`] and [`Resolve::push_file`]. This method will
-    /// automatically call the appropriate method based on what kind of
-    /// filesystem entry `path` is.
+    /// In all of these cases packages are allowed to depend on previously
+    /// inserted packages into this `Resolve`. Resolution for packages is based
+    /// on the name of each package and reference.
     ///
-    /// Returns the top-level [`PackageId`] as well as a list of all files read
-    /// during this parse.
+    /// This method returns a list of `PackageId` elements and additionally a
+    /// list of `PathBuf` elements. The `PackageId` elements represent the "main
+    /// package" that was parsed. For example if a single WIT file was specified
+    /// this will be all the packages found in the file. For a directory this
+    /// will be all the packages in the directory itself, but not in the `deps`
+    /// directory. The list of `PackageId` values is useful to pass to
+    /// [`Resolve::select_world`] to take a user-specified world in a
+    /// conventional fashion and select which to use for bindings generation.
+    ///
+    /// The returned list of `PathBuf` elements represents all files parsed
+    /// during this operation. This can be useful for systems that want to
+    /// rebuild or regenerate bindings based on files modified.
+    ///
+    /// More information can also be found at [`Resolve::push_dir`] and
+    /// [`Resolve::push_file`].
     pub fn push_path(&mut self, path: impl AsRef<Path>) -> Result<(Vec<PackageId>, Vec<PathBuf>)> {
         self._push_path(path.as_ref())
     }
@@ -247,25 +259,42 @@ impl Resolve {
     }
 
     /// Parses the filesystem directory at `path` as a WIT package and returns
-    /// the fully resolved [`PackageId`] as a result.
+    /// a fully resolved [`PackageId`] list as a result.
     ///
     /// The directory itself is parsed with [`UnresolvedPackageGroup::parse_dir`]
-    /// which has more information on the layout of the directory. This method,
-    /// however, additionally supports an optional `deps` dir where dependencies
-    /// can be located.
-    ///
-    /// All entries in the `deps` directory are inspected and parsed as follows:
-    ///
-    /// * Any directories inside of `deps` are assumed to be another WIT package
-    ///   and are parsed with [`UnresolvedPackageGroup::parse_dir`].
-    /// * WIT files (`*.wit`) are parsed with [`UnresolvedPackageGroup::parse_file`].
-    /// * WebAssembly files (`*.wasm` or `*.wat`) are assumed to be WIT packages
-    ///   encoded to wasm and are parsed and inserted into `self`.
-    ///
-    /// This function returns the [`PackageId`]s of the root parsed packages at
-    /// `path`, along with a list of all paths that were consumed during parsing
-    /// for the root package and all dependency packages, for each package encountered.
-    pub fn push_dir(&mut self, path: &Path) -> Result<(Vec<PackageId>, Vec<PathBuf>)> {
+    /// and then all packages found are inserted into this `Resolve`. The `path`
+    /// specified may have a `deps` subdirectory which is probed automatically
+    /// for any other WIT dependencies.
+    ///
+    /// The `deps` folder may contain:
+    ///
+    /// * `$path/deps/my-package/*.wit` - a directory that may contain multiple
+    ///   WIT files. This is parsed with [`UnresolvedPackageGroup::parse_dir`]
+    ///   and then inserted into this [`Resolve`]. Note that cannot recursively
+    ///   contain a `deps` directory.
+    /// * `$path/deps/my-package.wit` - a single-file WIT package. This is
+    ///   parsed with [`Resolve::push_file`] and then added to `self` for
+    ///   name reoslution.
+    /// * `$path/deps/my-package.{wasm,wat}` - a wasm-encoded WIT package either
+    ///   in the text for binary format.
+    ///
+    /// In all cases entries in the `deps` folder are added to `self` first
+    /// before adding files found in `path` itself. All WIT packages found are
+    /// candidates for name-based resolution that other packages may used.
+    ///
+    /// This function returns a tuple of two values. The first value is a list
+    /// of [`PackageId`] values which represents the WIT packages found within
+    /// `path`, but not those within `deps`. The `path` provided may contain
+    /// only a single WIT package but might also use the multi-package form of
+    /// WIT, and the returned list will indicate which was used. This argument
+    /// is useful for passing to [`Resolve::select_world`] for choosing
+    /// something to bindgen with.
+    ///
+    /// The second value returned here is the list of paths that were parsed
+    /// when generating the return value. This can be useful for build systems
+    /// that want to rebuild bindings whenever one of the files change.
+    pub fn push_dir(&mut self, path: impl AsRef<Path>) -> Result<(Vec<PackageId>, Vec<PathBuf>)> {
+        let path = path.as_ref();
         let deps_path = path.join("deps");
         let unresolved_deps = self.parse_deps_dir(&deps_path).with_context(|| {
             format!(
@@ -342,13 +371,14 @@ impl Resolve {
     ///   In this the package and all of its dependencies are automatically
     ///   inserted into `self`.
     ///
-    /// In both situations the `PackageId` of the resulting resolved package is
-    /// returned from this method.
+    /// In both situations the `PackageId`s of the resulting resolved packages
+    /// are returned from this method. The return value is mostly useful in
+    /// conjunction with [`Resolve::select_world`].
     pub fn push_file(&mut self, path: impl AsRef<Path>) -> Result<Vec<PackageId>> {
         match self._push_file(path.as_ref())? {
             #[cfg(feature = "decoding")]
             ParsedFile::Package(id) => Ok(vec![id]),
-            ParsedFile::Unresolved(pkgs) => self.append(pkgs),
+            ParsedFile::Unresolved(pkgs) => self.push_group(pkgs),
         }
     }
 
@@ -413,22 +443,32 @@ impl Resolve {
         source_map.rewrite_error(|| Remap::default().append(self, unresolved))
     }
 
-    /// Appends new [`UnresolvedPackageSet`] to this [`Resolve`], creating a
+    /// Appends new [`UnresolvedPackageGroup`] to this [`Resolve`], creating a
     /// fully resolved package with no dangling references.
     ///
-    /// The `deps` argument indicates that the named dependencies in
-    /// `unresolved` to packages are resolved by the mapping specified.
-    ///
     /// Any dependency resolution error or otherwise world-elaboration error
     /// will be returned here, if successful a package identifier is returned
     /// which corresponds to the package that was just inserted.
     ///
-    /// The returned [PackageId]s are listed in topologically sorted order.
-    pub fn append(&mut self, unresolved_groups: UnresolvedPackageGroup) -> Result<Vec<PackageId>> {
+    /// The returned [`PackageId`]s are listed in topologically sorted order.
+    pub fn push_group(
+        &mut self,
+        unresolved_groups: UnresolvedPackageGroup,
+    ) -> Result<Vec<PackageId>> {
         let (pkg_ids, _) = self.sort_unresolved_packages(vec![unresolved_groups])?;
         Ok(pkg_ids)
     }
 
+    /// Convenience method for combining [`UnresolvedPackageGroup::parse`] and
+    /// [`Resolve::push_group`].
+    ///
+    /// The `path` provided is used for error messages but otherwise is not
+    /// read. This method does not touch the filesystem. The `contents` provided
+    /// are the contents of a WIT package.
+    pub fn push_str(&mut self, path: impl AsRef<Path>, contents: &str) -> Result<Vec<PackageId>> {
+        self.push_group(UnresolvedPackageGroup::parse(path.as_ref(), contents)?)
+    }
+
     pub fn all_bits_valid(&self, ty: &Type) -> bool {
         match ty {
             Type::U8
@@ -798,46 +838,154 @@ impl Resolve {
         base
     }
 
-    /// Attempts to locate a world given the "default" package `pkg` and the
+    /// Attempts to locate a world given the "default" set of `packages` and the
     /// optional string specifier `world`.
     ///
     /// This method is intended to be used by bindings generation tools to
-    /// select a world from either `pkg` or a package in this `Resolve`.
+    /// select a world from either `packages` or a package in this `Resolve`.
+    /// The `packages` list is a return value from methods such as
+    /// [`push_path`](Resolve::push_path), [`push_dir`](Resolve::push_dir),
+    /// [`push_file`](Resolve::push_file), [`push_group`](Resolve::push_group),
+    /// or [`push_str`](Resolve::push_str). The return values of those methods
+    /// are the "main package list" which is specified by the user and is used
+    /// as a heuristic for world selection.
     ///
-    /// If `world` is `None` then `pkg` must have precisely one world which will
-    /// be returned.
+    /// If `world` is `None` then `packages` must have one entry and that
+    /// package must have exactly one world. If this is the case then that world
+    /// will be returned, otherwise an error will be returned.
     ///
     /// If `world` is `Some` then it can either be:
     ///
-    /// * A kebab-name of a world contained within `pkg` which is being
-    ///   selected, such as `"the-world"`.
+    /// * A kebab-name of a world such as `"the-world"`. In this situation
+    ///   the `packages` list must have only a single entry. If `packages` has
+    ///   no entries or more than one, or if the kebab-name does not exist in
+    ///   the one package specified, then an error will be returned.
     ///
     /// * An ID-based form of a world which is selected within this `Resolve`,
-    ///   ignoring `pkg`. For example `"wasi:http/proxy"`.
+    ///   for example `"wasi:http/proxy"`. In this situation the `packages`
+    ///   array is ignored and the ID specified is use to lookup a package. Note
+    ///   that a version does not need to be specified in this string if there's
+    ///   only one package of the same name and it has a version. In this
+    ///   situation the version can be omitted.
     ///
     /// If successful the corresponding `WorldId` is returned, otherwise an
     /// error is returned.
-    pub fn select_world(&self, pkg: PackageId, world: Option<&str>) -> Result<WorldId> {
-        let world = match world {
-            Some(world) => world,
-            None => {
-                let pkg = &self.packages[pkg];
-                match pkg.worlds.len() {
-                    0 => bail!("no worlds found in package `{}`", pkg.name),
-                    1 => return Ok(*pkg.worlds.values().next().unwrap()),
-                    _ => bail!(
-                        "multiple worlds found in package `{}`: one must be explicitly chosen",
-                        pkg.name
-                    ),
-                }
-            }
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use anyhow::Result;
+    /// use wit_parser::Resolve;
+    ///
+    /// fn main() -> Result<()> {
+    ///     let mut resolve = Resolve::default();
+    ///
+    ///     // For inputs which have a single package and only one world `None`
+    ///     // can be specified.
+    ///     let ids = resolve.push_str(
+    ///         "./my-test.wit",
+    ///         r#"
+    ///             package example:wit1;
+    ///
+    ///             world foo {
+    ///                 // ...
+    ///             }
+    ///         "#,
+    ///     )?;
+    ///     assert!(resolve.select_world(&ids, None).is_ok());
+    ///
+    ///     // For inputs which have a single package and multiple worlds then
+    ///     // a world must be specified.
+    ///     let ids = resolve.push_str(
+    ///         "./my-test.wit",
+    ///         r#"
+    ///             package example:wit2;
+    ///
+    ///             world foo { /* ... */ }
+    ///
+    ///             world bar { /* ... */ }
+    ///         "#,
+    ///     )?;
+    ///     assert!(resolve.select_world(&ids, None).is_err());
+    ///     assert!(resolve.select_world(&ids, Some("foo")).is_ok());
+    ///     assert!(resolve.select_world(&ids, Some("bar")).is_ok());
+    ///
+    ///     // For inputs which have more than one package then a fully
+    ///     // qualified name must be specified.
+    ///     let ids = resolve.push_str(
+    ///         "./my-test.wit",
+    ///         r#"
+    ///             package example:wit3 {
+    ///                 world foo { /* ... */ }
+    ///             }
+    ///
+    ///             package example:wit4 {
+    ///                 world foo { /* ... */ }
+    ///             }
+    ///         "#,
+    ///     )?;
+    ///     assert!(resolve.select_world(&ids, None).is_err());
+    ///     assert!(resolve.select_world(&ids, Some("foo")).is_err());
+    ///     assert!(resolve.select_world(&ids, Some("example:wit3/foo")).is_ok());
+    ///     assert!(resolve.select_world(&ids, Some("example:wit4/foo")).is_ok());
+    ///
+    ///     // Note that the `ids` or `packages` argument is ignored if a fully
+    ///     // qualified world specified is provided meaning previous worlds
+    ///     // can be selected.
+    ///     assert!(resolve.select_world(&[], Some("example:wit1/foo")).is_ok());
+    ///     assert!(resolve.select_world(&[], Some("example:wit2/foo")).is_ok());
+    ///
+    ///     // When selecting with a version it's ok to drop the version when
+    ///     // there's only a single copy of that package in `Resolve`.
+    ///     resolve.push_str(
+    ///         "./my-test.wit",
+    ///         r#"
+    ///             package example:wit5@1.0.0;
+    ///
+    ///             world foo { /* ... */ }
+    ///         "#,
+    ///     )?;
+    ///     assert!(resolve.select_world(&[], Some("example:wit5/foo")).is_ok());
+    ///
+    ///     // However when a single package has multiple versions in a resolve
+    ///     // it's required to specify the version to select which one.
+    ///     resolve.push_str(
+    ///         "./my-test.wit",
+    ///         r#"
+    ///             package example:wit5@2.0.0;
+    ///
+    ///             world foo { /* ... */ }
+    ///         "#,
+    ///     )?;
+    ///     assert!(resolve.select_world(&[], Some("example:wit5/foo")).is_err());
+    ///     assert!(resolve.select_world(&[], Some("example:wit5/foo@1.0.0")).is_ok());
+    ///     assert!(resolve.select_world(&[], Some("example:wit5/foo@2.0.0")).is_ok());
+    ///
+    ///     Ok(())
+    /// }
+    /// ```
+    pub fn select_world(&self, packages: &[PackageId], world: Option<&str>) -> Result<WorldId> {
+        let world_path = match world {
+            Some(world) => Some(
+                parse_use_path(world)
+                    .with_context(|| format!("failed to parse world specifier `{world}`"))?,
+            ),
+            None => None,
         };
 
-        let path = parse_use_path(world)
-            .with_context(|| format!("failed to parse world specifier `{world}`"))?;
-        let (pkg, world) = match path {
-            ParsedUsePath::Name(name) => (pkg, name),
-            ParsedUsePath::Package(pkg, interface) => {
+        let (pkg, world_name) = match world_path {
+            Some(ParsedUsePath::Name(name)) => match packages {
+                [] => bail!("no packages were found to locate the world `{name}` within"),
+                [one] => (*one, name),
+                [..] => {
+                    bail!(
+                        "the supplied WIT source files describe multiple packages; \
+                         please provide a fully-qualified world-specifier select \
+                         a world amongst these packages"
+                    )
+                }
+            },
+            Some(ParsedUsePath::Package(pkg, interface)) => {
                 let pkg = match self.package_names.get(&pkg) {
                     Some(pkg) => *pkg,
                     None => {
@@ -863,14 +1011,35 @@ impl Resolve {
                         }
                     }
                 };
-                (pkg, interface)
-            }
+                (pkg, interface.to_string())
+            }
+            None => match packages {
+                [] => bail!("no packages were specified nor is a world specified"),
+                [one] => {
+                    let pkg = &self.packages[*one];
+                    match pkg.worlds.len() {
+                        0 => bail!("no worlds found in package `{}`", pkg.name),
+                        1 => return Ok(*pkg.worlds.values().next().unwrap()),
+                        _ => bail!(
+                            "multiple worlds found in package `{}`: one must be explicitly chosen",
+                            pkg.name
+                        ),
+                    }
+                }
+                [..] => {
+                    bail!(
+                        "the supplied WIT source files describe multiple packages; \
+                         please provide a fully-qualified world-specifier select \
+                         a world amongst these packages"
+                    )
+                }
+            },
         };
         let pkg = &self.packages[pkg];
         pkg.worlds
-            .get(&world)
+            .get(&world_name)
             .copied()
-            .ok_or_else(|| anyhow!("no world named `{world}` in package"))
+            .ok_or_else(|| anyhow!("no world named `{world_name}` in package"))
     }
 
     /// Assigns a human readable name to the `WorldKey` specified.
@@ -2512,63 +2681,60 @@ impl<'a> MergeMap<'a> {
 
 #[cfg(test)]
 mod tests {
-    use crate::{PackageId, Resolve};
+    use crate::Resolve;
+    use anyhow::Result;
 
     #[test]
-    fn select_world() {
+    fn select_world() -> Result<()> {
         let mut resolve = Resolve::default();
-        parse_into(
-            &mut resolve,
+        resolve.push_str(
+            "test.wit",
             r#"
                 package foo:bar@0.1.0;
 
                 world foo {}
             "#,
-        );
-        parse_into(
-            &mut resolve,
+        )?;
+        resolve.push_str(
+            "test.wit",
             r#"
                 package foo:baz@0.1.0;
 
                 world foo {}
             "#,
-        );
-        parse_into(
-            &mut resolve,
+        )?;
+        resolve.push_str(
+            "test.wit",
             r#"
                 package foo:baz@0.2.0;
 
                 world foo {}
             "#,
-        );
+        )?;
 
-        let dummy = parse_into(
-            &mut resolve,
+        let dummy = resolve.push_str(
+            "test.wit",
             r#"
                 package foo:dummy;
 
                 world foo {}
             "#,
-        );
+        )?;
 
-        assert!(resolve.select_world(dummy, None).is_ok());
-        assert!(resolve.select_world(dummy, Some("xx")).is_err());
-        assert!(resolve.select_world(dummy, Some("")).is_err());
-        assert!(resolve.select_world(dummy, Some("foo:bar/foo")).is_ok());
+        assert!(resolve.select_world(&dummy, None).is_ok());
+        assert!(resolve.select_world(&dummy, Some("xx")).is_err());
+        assert!(resolve.select_world(&dummy, Some("")).is_err());
+        assert!(resolve.select_world(&dummy, Some("foo:bar/foo")).is_ok());
         assert!(resolve
-            .select_world(dummy, Some("foo:bar/foo@0.1.0"))
+            .select_world(&dummy, Some("foo:bar/foo@0.1.0"))
             .is_ok());
-        assert!(resolve.select_world(dummy, Some("foo:baz/foo")).is_err());
+        assert!(resolve.select_world(&dummy, Some("foo:baz/foo")).is_err());
         assert!(resolve
-            .select_world(dummy, Some("foo:baz/foo@0.1.0"))
+            .select_world(&dummy, Some("foo:baz/foo@0.1.0"))
             .is_ok());
         assert!(resolve
-            .select_world(dummy, Some("foo:baz/foo@0.2.0"))
+            .select_world(&dummy, Some("foo:baz/foo@0.2.0"))
             .is_ok());
-    }
-
-    fn parse_into(resolve: &mut Resolve, wit: &str) -> PackageId {
-        let pkgs = crate::UnresolvedPackageGroup::parse("input.wit".as_ref(), wit).unwrap();
-        resolve.append(pkgs).unwrap()[0]
+        Ok(())
     }
 }
diff --git a/fuzz/src/roundtrip_wit.rs b/fuzz/src/roundtrip_wit.rs
index 19967992..b15cd8c4 100644
--- a/fuzz/src/roundtrip_wit.rs
+++ b/fuzz/src/roundtrip_wit.rs
@@ -90,7 +90,7 @@ fn roundtrip_through_printing(file: &str, resolve: &Resolve, wasm: &[u8]) {
         write_file(&format!("{file}-{pkg_name}.wit"), &doc);
         map.push(format!("{pkg_name}.wit").as_ref(), doc);
         let unresolved = map.parse().unwrap();
-        let id = new_resolve.append(unresolved).unwrap();
+        let id = new_resolve.push_group(unresolved).unwrap();
         last = Some(id.last().unwrap().to_owned());
     }
 
diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs
index 5d759df4..d546f828 100644
--- a/src/bin/wasm-tools/component.rs
+++ b/src/bin/wasm-tools/component.rs
@@ -11,10 +11,9 @@ use wasm_tools::Output;
 use wasmparser::WasmFeatures;
 use wat::Detect;
 use wit_component::{
-    embed_component_metadata, resolve_world_from_name, ComponentEncoder, DecodedWasm, Linker,
-    StringEncoding, WitPrinter,
+    embed_component_metadata, ComponentEncoder, DecodedWasm, Linker, StringEncoding, WitPrinter,
 };
-use wit_parser::{PackageId, Resolve, UnresolvedPackageGroup};
+use wit_parser::{PackageId, Resolve};
 
 /// WebAssembly wit-based component tooling.
 #[derive(Parser)]
@@ -285,7 +284,7 @@ impl EmbedOpts {
             Some(self.io.parse_input_wasm()?)
         };
         let (resolve, pkg_ids) = self.resolve.load()?;
-        let world = resolve_world_from_name(&resolve, pkg_ids, self.world.as_deref())?;
+        let world = resolve.select_world(&pkg_ids, self.world.as_deref())?;
         let mut wasm = wasm.unwrap_or_else(|| wit_component::dummy_module(&resolve, world));
 
         embed_component_metadata(
@@ -590,8 +589,7 @@ impl WitOpts {
                 };
                 let mut resolve =
                     WitResolve::resolve_with_features(&self.features, self.all_features);
-                let pkgs = UnresolvedPackageGroup::parse(path, input)?;
-                let ids = resolve.append(pkgs)?;
+                let ids = resolve.push_str(path, input)?;
                 Ok(DecodedWasm::WitPackages(resolve, ids))
             }
         }
@@ -723,7 +721,7 @@ impl TargetsOpts {
     /// Executes the application.
     fn run(self) -> Result<()> {
         let (resolve, pkg_ids) = self.resolve.load()?;
-        let world = resolve_world_from_name(&resolve, pkg_ids, self.world.as_deref())?;
+        let world = resolve.select_world(&pkg_ids, self.world.as_deref())?;
         let component_to_test = self.input.parse_wasm()?;
 
         wit_component::targets(&resolve, world, &component_to_test)?;
@@ -763,8 +761,8 @@ impl SemverCheckOpts {
 
     fn run(self) -> Result<()> {
         let (resolve, pkg_ids) = self.resolve.load()?;
-        let prev = resolve_world_from_name(&resolve, pkg_ids.clone(), Some(self.prev).as_deref())?;
-        let new = resolve_world_from_name(&resolve, pkg_ids, Some(self.new).as_deref())?;
+        let prev = resolve.select_world(&pkg_ids, Some(self.prev.as_str()))?;
+        let new = resolve.select_world(&pkg_ids, Some(self.new.as_str()))?;
         wit_component::semver_check(resolve, prev, new)?;
         Ok(())
     }


Test patch:
diff --git a/crates/wit-component/tests/components.rs b/crates/wit-component/tests/components.rs
index 87917cce..de2ff79c 100644
--- a/crates/wit-component/tests/components.rs
+++ b/crates/wit-component/tests/components.rs
@@ -245,7 +245,7 @@ fn read_core_module(path: &Path, resolve: &Resolve, pkg: PackageId) -> Result<Ve
     let mut wasm = wat::parse_file(path)?;
     let name = path.file_stem().and_then(|s| s.to_str()).unwrap();
     let world = resolve
-        .select_world(pkg, Some(name))
+        .select_world(&[pkg], Some(name))
         .context("failed to select a world")?;
 
     // Add this producer data to the wit-component metadata so we can make sure it gets through the
diff --git a/crates/wit-component/tests/interfaces.rs b/crates/wit-component/tests/interfaces.rs
index 926499db..87c7ac2d 100644
--- a/crates/wit-component/tests/interfaces.rs
+++ b/crates/wit-component/tests/interfaces.rs
@@ -51,7 +51,7 @@ fn run_test(path: &Path, is_dir: bool) -> Result<()> {
     let packages = if is_dir {
         resolve.push_dir(path)?.0
     } else {
-        resolve.append(UnresolvedPackageGroup::parse_file(path)?)?
+        resolve.push_file(path)?
     };
 
     for package in packages {
@@ -107,8 +107,7 @@ fn assert_print(resolve: &Resolve, pkg_ids: &[PackageId], path: &Path, is_dir: b
         assert_output(&expected, &output)?;
     }
 
-    UnresolvedPackageGroup::parse("foo.wit".as_ref(), &output)
-        .context("failed to parse printed output")?;
+    UnresolvedPackageGroup::parse("foo.wit", &output).context("failed to parse printed output")?;
     Ok(())
 }
 
diff --git a/crates/wit-component/tests/linking.rs b/crates/wit-component/tests/linking.rs
index 56757f97..f1d9ac35 100644
--- a/crates/wit-component/tests/linking.rs
+++ b/crates/wit-component/tests/linking.rs
@@ -1,8 +1,7 @@
 use {
     anyhow::{Context, Result},
-    std::path::Path,
     wit_component::StringEncoding,
-    wit_parser::{Resolve, UnresolvedPackageGroup},
+    wit_parser::Resolve,
 };
 
 const FOO: &str = r#"
@@ -141,12 +140,8 @@ fn encode(wat: &str, wit: Option<&str>) -> Result<Vec<u8>> {
 
     if let Some(wit) = wit {
         let mut resolve = Resolve::default();
-        let UnresolvedPackageGroup {
-            mut packages,
-            source_map,
-        } = UnresolvedPackageGroup::parse(Path::new("wit"), wit)?;
-        let pkg = resolve.push(packages.remove(0), &source_map)?;
-        let world = resolve.select_world(pkg, None)?;
+        let pkgs = resolve.push_str("test.wit", wit)?;
+        let world = resolve.select_world(&pkgs, None)?;
 
         wit_component::embed_component_metadata(
             &mut module,
diff --git a/crates/wit-component/tests/targets.rs b/crates/wit-component/tests/targets.rs
index 8dac12d3..7c9581a5 100644
--- a/crates/wit-component/tests/targets.rs
+++ b/crates/wit-component/tests/targets.rs
@@ -1,6 +1,6 @@
-use anyhow::{bail, Context, Result};
+use anyhow::{Context, Result};
 use std::{fs, path::Path};
-use wit_parser::{Resolve, UnresolvedPackageGroup, WorldId};
+use wit_parser::{Resolve, WorldId};
 
 /// Tests whether a component targets a world.
 ///
@@ -22,7 +22,7 @@ use wit_parser::{Resolve, UnresolvedPackageGroup, WorldId};
 /// Run the test with the environment variable `BLESS` set to update `error.txt`.
 ///
 /// Each test is effectively executing as:
-/// ```wasm-tools component targets -w foobar test.wit test.wat```
+/// `wasm-tools component targets -w foobar test.wit test.wat`
 #[test]
 fn targets() -> Result<()> {
     drop(env_logger::try_init());
@@ -79,23 +79,10 @@ fn load_test_wit(path: &Path) -> Result<(Resolve, WorldId)> {
     const TEST_TARGET_WORLD_ID: &str = "foobar";
 
     let test_wit_path = path.join("test.wit");
-    let UnresolvedPackageGroup {
-        mut packages,
-        source_map,
-    } = UnresolvedPackageGroup::parse_file(&test_wit_path)
-        .context("failed to parse WIT package")?;
-    if packages.is_empty() {
-        bail!("Files were completely empty - are you sure these are the files you're looking for?")
-    }
-    if packages.len() > 1 {
-        bail!("Multi-package targeting tests are not yet supported.")
-    }
-
     let mut resolve = Resolve::default();
-    let package_id = resolve.push(packages.remove(0), &source_map)?;
-
+    let pkgs = resolve.push_file(&test_wit_path)?;
     let world_id = resolve
-        .select_world(package_id, Some(TEST_TARGET_WORLD_ID))
+        .select_world(&pkgs, Some(TEST_TARGET_WORLD_ID))
         .with_context(|| "failed to select world from package".to_string())?;
 
     Ok((resolve, world_id))
diff --git a/crates/wit-component/tests/wit.rs b/crates/wit-component/tests/wit.rs
index 860fd108..1930eabf 100644
--- a/crates/wit-component/tests/wit.rs
+++ b/crates/wit-component/tests/wit.rs
@@ -9,9 +9,9 @@ fn parse_wit_dir() -> Result<()> {
     drop(env_logger::try_init());
 
     let mut resolver = Resolve::default();
-    let package_id = resolver.push_path("tests/wit/parse-dir/wit")?.0[0];
+    let (package_ids, _) = resolver.push_path("tests/wit/parse-dir/wit")?;
     assert!(resolver
-        .select_world(package_id, "foo-world".into())
+        .select_world(&package_ids, "foo-world".into())
         .is_ok());
 
     Ok(())
@@ -23,10 +23,8 @@ fn parse_wit_file() -> Result<()> {
     drop(env_logger::try_init());
 
     let mut resolver = Resolve::default();
-    let package_id = resolver
-        .push_path("tests/wit/parse-dir/wit/deps/bar/bar.wit")?
-        .0[0];
-    resolver.select_world(package_id, "bar-world".into())?;
+    let (package_ids, _) = resolver.push_path("tests/wit/parse-dir/wit/deps/bar/bar.wit")?;
+    resolver.select_world(&package_ids, "bar-world".into())?;
     assert!(resolver
         .interfaces
         .iter()