diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index 336266dead..ddccdadd51 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -2157,7 +2157,8 @@ world test { } "#, ) - .unwrap(), + .unwrap() + .remove(0), ) .unwrap(); diff --git a/crates/wit-component/src/lib.rs b/crates/wit-component/src/lib.rs index 9b3be71c59..7b9851d8ee 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, Result}; +use anyhow::{bail, Context, Result}; use wasm_encoder::{CanonicalOption, Encode, Section}; -use wit_parser::{Resolve, WorldId}; +use wit_parser::{parse_use_path, PackageId, ParsedUsePath, Resolve, WorldId}; mod encoding; mod gc; @@ -79,6 +79,43 @@ impl From 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, + world_name: Option<&str>, +) -> Result { + 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 { @@ -147,7 +184,7 @@ world test-world {} // Parse pre-canned WIT to build resolver let mut resolver = Resolve::default(); - let pkg = UnresolvedPackage::parse(&Path::new("in-code.wit"), COMPONENT_WIT)?; + let pkg = UnresolvedPackage::parse(&Path::new("in-code.wit"), COMPONENT_WIT)?.remove(0); let pkg_id = resolver.push(pkg)?; let world = resolver.select_world(pkg_id, Some("test-world").into())?; diff --git a/crates/wit-component/src/metadata.rs b/crates/wit-component/src/metadata.rs index 0fdc7e5d9c..59cb7353ee 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::{DecodedWasm, StringEncoding}; +use crate::{resolve_world_from_name, DecodedWasm, StringEncoding}; use anyhow::{bail, Context, Result}; use indexmap::IndexMap; use std::borrow::Cow; @@ -259,12 +259,12 @@ impl Bindgen { let world_name = reader.read_string()?; wasm = &data[reader.original_position()..]; - let (r, pkg) = match crate::decode(wasm)? { - DecodedWasm::WitPackage(resolve, pkg) => (resolve, pkg), - DecodedWasm::Component(..) => bail!("expected an encoded wit package"), + let (r, pkgs) = match crate::decode(wasm)? { + DecodedWasm::WitPackages(resolve, pkgs) => (resolve, pkgs), + DecodedWasm::Component(..) => bail!("expected encoded wit package(s)"), }; resolve = r; - world = resolve.packages[pkg].worlds[world_name]; + world = resolve_world_from_name(&resolve, pkgs, Some(world_name.into()))?; } // Current format where `data` is a wasm component itself. diff --git a/crates/wit-component/src/printing.rs b/crates/wit-component/src/printing.rs index 33be432dc1..cb0488ac92 100644 --- a/crates/wit-component/src/printing.rs +++ b/crates/wit-component/src/printing.rs @@ -50,37 +50,43 @@ impl WitPrinter { self } - /// Print the given WIT package to a string. - pub fn print(&mut self, resolve: &Resolve, pkgid: PackageId) -> Result { - let pkg = &resolve.packages[pkgid]; - self.print_docs(&pkg.docs); - self.output.push_str("package "); - self.print_name(&pkg.name.namespace); - self.output.push_str(":"); - self.print_name(&pkg.name.name); - if let Some(version) = &pkg.name.version { - self.output.push_str(&format!("@{version}")); - } - self.print_semicolon(); - self.output.push_str("\n\n"); - for (name, id) in pkg.interfaces.iter() { - self.print_docs(&resolve.interfaces[*id].docs); - self.print_stability(&resolve.interfaces[*id].stability); - self.output.push_str("interface "); - self.print_name(name); - self.output.push_str(" {\n"); - self.print_interface(resolve, *id)?; - writeln!(&mut self.output, "}}\n")?; - } + /// Print a set of one or more WIT packages into a string. + pub fn print(&mut self, resolve: &Resolve, pkg_ids: Vec) -> Result { + for (i, pkg_id) in pkg_ids.into_iter().enumerate() { + if i > 0 { + self.output.push_str("\n\n"); + } - for (name, id) in pkg.worlds.iter() { - self.print_docs(&resolve.worlds[*id].docs); - self.print_stability(&resolve.worlds[*id].stability); - self.output.push_str("world "); - self.print_name(name); - self.output.push_str(" {\n"); - self.print_world(resolve, *id)?; - writeln!(&mut self.output, "}}")?; + let pkg = &resolve.packages[pkg_id]; + self.print_docs(&pkg.docs); + self.output.push_str("package "); + self.print_name(&pkg.name.namespace); + self.output.push_str(":"); + self.print_name(&pkg.name.name); + if let Some(version) = &pkg.name.version { + self.output.push_str(&format!("@{version}")); + } + self.print_semicolon(); + self.output.push_str("\n\n"); + for (name, id) in pkg.interfaces.iter() { + self.print_docs(&resolve.interfaces[*id].docs); + self.print_stability(&resolve.interfaces[*id].stability); + self.output.push_str("interface "); + self.print_name(name); + self.output.push_str(" {\n"); + self.print_interface(resolve, *id)?; + writeln!(&mut self.output, "}}\n")?; + } + + for (name, id) in pkg.worlds.iter() { + self.print_docs(&resolve.worlds[*id].docs); + self.print_stability(&resolve.worlds[*id].stability); + self.output.push_str("world "); + self.print_name(name); + self.output.push_str(" {\n"); + self.print_world(resolve, *id)?; + writeln!(&mut self.output, "}}")?; + } } Ok(std::mem::take(&mut self.output).into()) diff --git a/crates/wit-component/src/semver_check.rs b/crates/wit-component/src/semver_check.rs index 66d01566dc..011b108d13 100644 --- a/crates/wit-component/src/semver_check.rs +++ b/crates/wit-component/src/semver_check.rs @@ -2,7 +2,7 @@ use crate::{ dummy_module, embed_component_metadata, encoding::encode_world, ComponentEncoder, StringEncoding, }; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use wasm_encoder::{ComponentBuilder, ComponentExportKind, ComponentTypeRef}; use wasmparser::{Validator, WasmFeatures}; use wit_parser::{Resolve, WorldId}; @@ -47,6 +47,18 @@ pub fn semver_check(mut resolve: Resolve, prev: WorldId, new: WorldId) -> Result pkg.name.version = None; } + let old_pkg_id = resolve.worlds[prev] + .package + .context("old world not in named package")?; + let old_pkg_name = &resolve.packages[old_pkg_id].name; + let new_pkg_id = resolve.worlds[new] + .package + .context("new world not in named package")?; + let new_pkg_name = &resolve.packages[new_pkg_id].name; + if old_pkg_id != new_pkg_id { + bail!("the old world is in package {old_pkg_name}, which is not the same as the new world, which is in package {new_pkg_name}", ) + } + // Component that will be validated at the end. let mut root_component = ComponentBuilder::default(); diff --git a/crates/wit-component/tests/components.rs b/crates/wit-component/tests/components.rs index 91f3051b69..56a659d16d 100644 --- a/crates/wit-component/tests/components.rs +++ b/crates/wit-component/tests/components.rs @@ -75,126 +75,141 @@ fn main() -> Result<()> { fn run_test(path: &Path) -> Result<()> { let test_case = path.file_stem().unwrap().to_str().unwrap(); - let mut resolve = Resolve::default(); - let (pkg, _) = resolve - .push_dir(&path) - .context("failed to push directory into resolve")?; + let pkgs = resolve.push_dir(&path)?; + let pkg_count = pkgs.len(); - let module_path = path.join("module.wat"); - let mut adapters = glob::glob(path.join("adapt-*.wat").to_str().unwrap())?; - let result = if module_path.is_file() { - let module = read_core_module(&module_path, &resolve, pkg) - .with_context(|| format!("failed to read core module at {module_path:?}"))?; - adapters - .try_fold( - ComponentEncoder::default().module(&module)?.validate(true), - |encoder, path| { - let (name, wasm) = read_name_and_module("adapt-", &path?, &resolve, pkg)?; - Ok::<_, Error>(encoder.adapter(&name, &wasm)?) - }, - )? - .encode() - } else { - let mut libs = glob::glob(path.join("lib-*.wat").to_str().unwrap())? - .map(|path| Ok(("lib-", path?, false))) - .chain( - glob::glob(path.join("dlopen-lib-*.wat").to_str().unwrap())? - .map(|path| Ok(("dlopen-lib-", path?, true))), - ) - .collect::>>()?; + for (pkg_id, _) in pkgs { + // If this test case contained multiple packages, create separate sub-directories for + // each. + let mut path = path.to_path_buf(); + if pkg_count > 1 { + let pkg_name = &resolve.packages[pkg_id].name; + path.push(pkg_name.namespace.clone()); + path.push(pkg_name.name.clone()); + fs::create_dir_all(path.clone())?; + } - // Sort list to ensure deterministic order, which determines priority in cases of duplicate symbols: - libs.sort_by(|(_, a, _), (_, b, _)| a.cmp(b)); + let module_path = path.join("module.wat"); + let mut adapters = glob::glob(path.join("adapt-*.wat").to_str().unwrap())?; + let result = if module_path.is_file() { + let module = read_core_module(&module_path, &resolve, pkg_id) + .with_context(|| format!("failed to read core module at {module_path:?}"))?; + adapters + .try_fold( + ComponentEncoder::default().module(&module)?.validate(true), + |encoder, path| { + let (name, wasm) = + read_name_and_module("adapt-", &path?, &resolve, pkg_id)?; + Ok::<_, Error>(encoder.adapter(&name, &wasm)?) + }, + )? + .encode() + } else { + let mut libs = glob::glob(path.join("lib-*.wat").to_str().unwrap())? + .map(|path| Ok(("lib-", path?, false))) + .chain( + glob::glob(path.join("dlopen-lib-*.wat").to_str().unwrap())? + .map(|path| Ok(("dlopen-lib-", path?, true))), + ) + .collect::>>()?; - let mut linker = Linker::default().validate(true); + // Sort list to ensure deterministic order, which determines priority in cases of duplicate symbols: + libs.sort_by(|(_, a, _), (_, b, _)| a.cmp(b)); - if path.join("stub-missing-functions").is_file() { - linker = linker.stub_missing_functions(true); - } + let mut linker = Linker::default().validate(true); - if path.join("use-built-in-libdl").is_file() { - linker = linker.use_built_in_libdl(true); - } + if path.join("stub-missing-functions").is_file() { + linker = linker.stub_missing_functions(true); + } - let linker = libs - .into_iter() - .try_fold(linker, |linker, (prefix, path, dl_openable)| { - let (name, wasm) = read_name_and_module(prefix, &path, &resolve, pkg)?; - Ok::<_, Error>(linker.library(&name, &wasm, dl_openable)?) - })?; + if path.join("use-built-in-libdl").is_file() { + linker = linker.use_built_in_libdl(true); + } - adapters - .try_fold(linker, |linker, path| { - let (name, wasm) = read_name_and_module("adapt-", &path?, &resolve, pkg)?; - Ok::<_, Error>(linker.adapter(&name, &wasm)?) - })? - .encode() - }; - let component_path = path.join("component.wat"); - let component_wit_path = path.join("component.wit.print"); - let error_path = path.join("error.txt"); + let linker = + libs.into_iter() + .try_fold(linker, |linker, (prefix, path, dl_openable)| { + let (name, wasm) = read_name_and_module(prefix, &path, &resolve, pkg_id)?; + Ok::<_, Error>(linker.library(&name, &wasm, dl_openable)?) + })?; - let bytes = match result { - Ok(bytes) => { - if test_case.starts_with("error-") { - bail!("expected an error but got success"); + adapters + .try_fold(linker, |linker, path| { + let (name, wasm) = read_name_and_module("adapt-", &path?, &resolve, pkg_id)?; + Ok::<_, Error>(linker.adapter(&name, &wasm)?) + })? + .encode() + }; + let component_path = path.join("component.wat"); + let component_wit_path = path.join("component.wit.print"); + let error_path = path.join("error.txt"); + + let bytes = match result { + Ok(bytes) => { + if test_case.starts_with("error-") { + bail!("expected an error but got success"); + } + bytes } - bytes - } - Err(err) => { - if !test_case.starts_with("error-") { - return Err(err); + Err(err) => { + if !test_case.starts_with("error-") { + return Err(err); + } + assert_output(&format!("{err:?}"), &error_path)?; + return Ok(()); } - assert_output(&format!("{err:?}"), &error_path)?; - return Ok(()); - } - }; + }; - let wat = wasmprinter::print_bytes(&bytes).context("failed to print bytes")?; - assert_output(&wat, &component_path)?; - let (pkg, resolve) = match wit_component::decode(&bytes).context("failed to decode resolve")? { - DecodedWasm::WitPackage(..) => unreachable!(), - DecodedWasm::Component(resolve, world) => (resolve.worlds[world].package.unwrap(), resolve), - }; - let wit = WitPrinter::default() - .print(&resolve, pkg) - .context("failed to print WIT")?; - assert_output(&wit, &component_wit_path)?; + let wat = wasmprinter::print_bytes(&bytes).context("failed to print bytes")?; + assert_output(&wat, &component_path)?; + let (pkg, resolve) = + match wit_component::decode(&bytes).context("failed to decode resolve")? { + DecodedWasm::WitPackages(..) => unreachable!(), + DecodedWasm::Component(resolve, world) => { + (resolve.worlds[world].package.unwrap(), resolve) + } + }; + let wit = WitPrinter::default() + .print(&resolve, vec![pkg]) + .context("failed to print WIT")?; + assert_output(&wit, &component_wit_path)?; - UnresolvedPackage::parse(&component_wit_path, &wit).context("failed to parse printed WIT")?; + UnresolvedPackage::parse(&component_wit_path, &wit) + .context("failed to parse printed WIT")?; - // Check that the producer data got piped through properly - let metadata = wasm_metadata::Metadata::from_binary(&bytes)?; - match metadata { - // Depends on the ComponentEncoder always putting the first module as the 0th child: - wasm_metadata::Metadata::Component { children, .. } => match children[0].as_ref() { - wasm_metadata::Metadata::Module { producers, .. } => { - let producers = producers.as_ref().expect("child module has producers"); - let processed_by = producers - .get("processed-by") - .expect("child has processed-by section"); - assert_eq!( - processed_by - .get("wit-component") - .expect("wit-component producer present"), - env!("CARGO_PKG_VERSION") - ); - if module_path.is_file() { + // Check that the producer data got piped through properly + let metadata = wasm_metadata::Metadata::from_binary(&bytes)?; + match metadata { + // Depends on the ComponentEncoder always putting the first module as the 0th child: + wasm_metadata::Metadata::Component { children, .. } => match children[0].as_ref() { + wasm_metadata::Metadata::Module { producers, .. } => { + let producers = producers.as_ref().expect("child module has producers"); + let processed_by = producers + .get("processed-by") + .expect("child has processed-by section"); assert_eq!( processed_by - .get("my-fake-bindgen") - .expect("added bindgen field present"), - "123.45" + .get("wit-component") + .expect("wit-component producer present"), + env!("CARGO_PKG_VERSION") ); - } else { - // Otherwise, we used `Linker`, which synthesizes the - // "main" module and thus won't have `my-fake-bindgen` + if module_path.is_file() { + assert_eq!( + processed_by + .get("my-fake-bindgen") + .expect("added bindgen field present"), + "123.45" + ); + } else { + // Otherwise, we used `Linker`, which synthesizes the + // "main" module and thus won't have `my-fake-bindgen` + } } - } - _ => panic!("expected child to be a module"), - }, - _ => panic!("expected top level metadata of component"), + _ => panic!("expected child to be a module"), + }, + _ => panic!("expected top level metadata of component"), + } } Ok(()) diff --git a/crates/wit-component/tests/components/multi-package/baz/qux/component.wat b/crates/wit-component/tests/components/multi-package/baz/qux/component.wat new file mode 100644 index 0000000000..b028617cac --- /dev/null +++ b/crates/wit-component/tests/components/multi-package/baz/qux/component.wat @@ -0,0 +1,90 @@ +(component + (type (;0;) + (instance + (type (;0;) s8) + (export (;1;) "x" (type (eq 0))) + (type (;2;) (list string)) + (type (;3;) (func (param "x" 2))) + (export (;0;) "qux1" (func (type 3))) + (type (;4;) (func)) + (export (;1;) "qux2" (func (type 4))) + (type (;5;) (func (param "x" 1))) + (export (;2;) "qux3" (func (type 5))) + ) + ) + (import "qux" (instance (;0;) (type 0))) + (core module (;0;) + (type (;0;) (func (param i32 i32))) + (type (;1;) (func)) + (type (;2;) (func (param i32))) + (type (;3;) (func (param i32 i32 i32 i32) (result i32))) + (import "qux" "qux1" (func (;0;) (type 0))) + (import "qux" "qux2" (func (;1;) (type 1))) + (import "qux" "qux3" (func (;2;) (type 2))) + (func (;3;) (type 3) (param i32 i32 i32 i32) (result i32) + unreachable + ) + (memory (;0;) 1) + (export "memory" (memory 0)) + (export "cabi_realloc" (func 3)) + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + (processed-by "my-fake-bindgen" "123.45") + ) + ) + (core module (;1;) + (type (;0;) (func (param i32 i32))) + (func $indirect-qux-qux1 (;0;) (type 0) (param i32 i32) + local.get 0 + local.get 1 + i32.const 0 + call_indirect (type 0) + ) + (table (;0;) 1 1 funcref) + (export "0" (func $indirect-qux-qux1)) + (export "$imports" (table 0)) + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) + ) + (core module (;2;) + (type (;0;) (func (param i32 i32))) + (import "" "0" (func (;0;) (type 0))) + (import "" "$imports" (table (;0;) 1 1 funcref)) + (elem (;0;) (i32.const 0) func 0) + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) + ) + (core instance (;0;) (instantiate 1)) + (alias core export 0 "0" (core func (;0;))) + (alias export 0 "qux2" (func (;0;))) + (core func (;1;) (canon lower (func 0))) + (alias export 0 "qux3" (func (;1;))) + (core func (;2;) (canon lower (func 1))) + (core instance (;1;) + (export "qux1" (func 0)) + (export "qux2" (func 1)) + (export "qux3" (func 2)) + ) + (core instance (;2;) (instantiate 0 + (with "qux" (instance 1)) + ) + ) + (alias core export 2 "memory" (core memory (;0;))) + (alias core export 2 "cabi_realloc" (core func (;3;))) + (alias core export 0 "$imports" (core table (;0;))) + (alias export 0 "qux1" (func (;2;))) + (core func (;4;) (canon lower (func 2) (memory 0) string-encoding=utf8)) + (core instance (;3;) + (export "$imports" (table 0)) + (export "0" (func 4)) + ) + (core instance (;4;) (instantiate 2 + (with "" (instance 3)) + ) + ) + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/components/multi-package/baz/qux/component.wit.print b/crates/wit-component/tests/components/multi-package/baz/qux/component.wit.print new file mode 100644 index 0000000000..71d2002137 --- /dev/null +++ b/crates/wit-component/tests/components/multi-package/baz/qux/component.wit.print @@ -0,0 +1,13 @@ +package root:component; + +world root { + import qux: interface { + type x = s8; + + qux1: func(x: list); + + qux2: func(); + + qux3: func(x: x); + } +} diff --git a/crates/wit-component/tests/components/multi-package/baz/qux/module.wat b/crates/wit-component/tests/components/multi-package/baz/qux/module.wat new file mode 100644 index 0000000000..8eb6e37d5a --- /dev/null +++ b/crates/wit-component/tests/components/multi-package/baz/qux/module.wat @@ -0,0 +1,7 @@ +(module + (import "qux" "qux1" (func (param i32 i32))) + (import "qux" "qux2" (func)) + (import "qux" "qux3" (func (param i32))) + (memory (export "memory") 1) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) unreachable) +) diff --git a/crates/wit-component/tests/components/multi-package/foo/bar/component.wat b/crates/wit-component/tests/components/multi-package/foo/bar/component.wat new file mode 100644 index 0000000000..82caf152ba --- /dev/null +++ b/crates/wit-component/tests/components/multi-package/foo/bar/component.wat @@ -0,0 +1,82 @@ +(component + (type (;0;) + (instance + (type (;0;) (record (field "a" u8))) + (export (;1;) "x" (type (eq 0))) + (type (;2;) (func (param "x" string))) + (export (;0;) "bar1" (func (type 2))) + (type (;3;) (func (param "x" 1))) + (export (;1;) "bar2" (func (type 3))) + ) + ) + (import "bar" (instance (;0;) (type 0))) + (core module (;0;) + (type (;0;) (func (param i32 i32))) + (type (;1;) (func (param i32))) + (type (;2;) (func (param i32 i32 i32 i32) (result i32))) + (import "bar" "bar1" (func (;0;) (type 0))) + (import "bar" "bar2" (func (;1;) (type 1))) + (func (;2;) (type 2) (param i32 i32 i32 i32) (result i32) + unreachable + ) + (memory (;0;) 1) + (export "memory" (memory 0)) + (export "cabi_realloc" (func 2)) + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + (processed-by "my-fake-bindgen" "123.45") + ) + ) + (core module (;1;) + (type (;0;) (func (param i32 i32))) + (func $indirect-bar-bar1 (;0;) (type 0) (param i32 i32) + local.get 0 + local.get 1 + i32.const 0 + call_indirect (type 0) + ) + (table (;0;) 1 1 funcref) + (export "0" (func $indirect-bar-bar1)) + (export "$imports" (table 0)) + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) + ) + (core module (;2;) + (type (;0;) (func (param i32 i32))) + (import "" "0" (func (;0;) (type 0))) + (import "" "$imports" (table (;0;) 1 1 funcref)) + (elem (;0;) (i32.const 0) func 0) + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) + ) + (core instance (;0;) (instantiate 1)) + (alias core export 0 "0" (core func (;0;))) + (alias export 0 "bar2" (func (;0;))) + (core func (;1;) (canon lower (func 0))) + (core instance (;1;) + (export "bar1" (func 0)) + (export "bar2" (func 1)) + ) + (core instance (;2;) (instantiate 0 + (with "bar" (instance 1)) + ) + ) + (alias core export 2 "memory" (core memory (;0;))) + (alias core export 2 "cabi_realloc" (core func (;2;))) + (alias core export 0 "$imports" (core table (;0;))) + (alias export 0 "bar1" (func (;1;))) + (core func (;3;) (canon lower (func 1) (memory 0) string-encoding=utf8)) + (core instance (;3;) + (export "$imports" (table 0)) + (export "0" (func 3)) + ) + (core instance (;4;) (instantiate 2 + (with "" (instance 3)) + ) + ) + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/components/multi-package/foo/bar/component.wit.print b/crates/wit-component/tests/components/multi-package/foo/bar/component.wit.print new file mode 100644 index 0000000000..f3cc24edf7 --- /dev/null +++ b/crates/wit-component/tests/components/multi-package/foo/bar/component.wit.print @@ -0,0 +1,13 @@ +package root:component; + +world root { + import bar: interface { + record x { + a: u8, + } + + bar1: func(x: string); + + bar2: func(x: x); + } +} diff --git a/crates/wit-component/tests/components/multi-package/foo/bar/module.wat b/crates/wit-component/tests/components/multi-package/foo/bar/module.wat new file mode 100644 index 0000000000..7a56c61661 --- /dev/null +++ b/crates/wit-component/tests/components/multi-package/foo/bar/module.wat @@ -0,0 +1,6 @@ +(module + (import "bar" "bar1" (func (param i32 i32))) + (import "bar" "bar2" (func (param i32))) + (memory (export "memory") 1) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) unreachable) +) diff --git a/crates/wit-component/tests/components/multi-package/module.wit b/crates/wit-component/tests/components/multi-package/module.wit new file mode 100644 index 0000000000..838ad7305b --- /dev/null +++ b/crates/wit-component/tests/components/multi-package/module.wit @@ -0,0 +1,24 @@ +package foo:bar { + world module { + import bar: interface { + record x { + a: u8 + } + + bar1: func(x: string); + bar2: func(x: x); + } + } +} + +package baz:qux { + world module { + import qux: interface { + type x = s8; + + qux1: func(x: list); + qux2: func(); + qux3: func(x: x); + } + } +} diff --git a/crates/wit-component/tests/interfaces.rs b/crates/wit-component/tests/interfaces.rs index 6cb7643901..56b75582e4 100644 --- a/crates/wit-component/tests/interfaces.rs +++ b/crates/wit-component/tests/interfaces.rs @@ -48,53 +48,69 @@ fn main() -> Result<()> { fn run_test(path: &Path, is_dir: bool) -> Result<()> { let mut resolve = Resolve::new(); - let package = if is_dir { - resolve.push_dir(path)?.0 + let packages = if is_dir { + resolve.push_dir(path)?.into_iter().map(|x| x.0).collect() } else { - resolve.push(UnresolvedPackage::parse_file(path)?)? + resolve.append(UnresolvedPackage::parse_file(path)?)? }; - assert_print(&resolve, package, path, is_dir)?; + for package in packages { + assert_print(&resolve, vec![package], path, is_dir)?; - let features = WasmFeatures::default() | WasmFeatures::COMPONENT_MODEL; + let features = WasmFeatures::default() | WasmFeatures::COMPONENT_MODEL; - // First convert the WIT package to a binary WebAssembly output, then - // convert that binary wasm to textual wasm, then assert it matches the - // expectation. - let wasm = wit_component::encode(Some(true), &resolve, package)?; - let wat = wasmprinter::print_bytes(&wasm)?; - assert_output(&path.with_extension("wat"), &wat)?; - wasmparser::Validator::new_with_features(features) - .validate_all(&wasm) - .context("failed to validate wasm output")?; + // First convert the WIT package to a binary WebAssembly output, then + // convert that binary wasm to textual wasm, then assert it matches the + // expectation. + let wasm = wit_component::encode(Some(true), &resolve, package)?; + let wat = wasmprinter::print_bytes(&wasm)?; + assert_output(&path.with_extension("wat"), &wat)?; + wasmparser::Validator::new_with_features(features) + .validate_all(&wasm) + .context("failed to validate wasm output")?; - // Next decode a fresh WIT package from the WebAssembly generated. Print - // this package's documents and assert they all match the expectations. - let decoded = wit_component::decode(&wasm)?; - let resolve = decoded.resolve(); + // Next decode a fresh WIT package from the WebAssembly generated. Print + // this package's documents and assert they all match the expectations. + let decoded = wit_component::decode(&wasm)?; + assert_eq!( + 1, + decoded.packages().len(), + "Each input WIT package should produce WASM that contains only one package" + ); - assert_print(resolve, decoded.package(), path, is_dir)?; + let decoded_package = decoded.packages()[0]; + let resolve = decoded.resolve(); - // Finally convert the decoded package to wasm again and make sure it - // matches the prior wasm. - let wasm2 = wit_component::encode(Some(true), resolve, decoded.package())?; - if wasm != wasm2 { - let wat2 = wasmprinter::print_bytes(&wasm)?; - assert_eq!(wat, wat2, "document did not roundtrip correctly"); + assert_print(resolve, decoded.packages(), path, is_dir)?; + + // Finally convert the decoded package to wasm again and make sure it + // matches the prior wasm. + let wasm2 = wit_component::encode(Some(true), resolve, decoded_package)?; + if wasm != wasm2 { + let wat2 = wasmprinter::print_bytes(&wasm)?; + assert_eq!(wat, wat2, "document did not roundtrip correctly"); + } } Ok(()) } -fn assert_print(resolve: &Resolve, package: PackageId, path: &Path, is_dir: bool) -> Result<()> { - let pkg = &resolve.packages[package]; - let expected = if is_dir { - path.join(format!("{}.wit.print", &pkg.name.name)) - } else { - path.with_extension("wit.print") - }; - let output = WitPrinter::default().print(resolve, package)?; - assert_output(&expected, &output)?; +fn assert_print( + resolve: &Resolve, + pkg_ids: Vec, + path: &Path, + is_dir: bool, +) -> Result<()> { + let output = WitPrinter::default().print(resolve, pkg_ids.clone())?; + for pkg_id in pkg_ids { + let pkg = &resolve.packages[pkg_id]; + let expected = if is_dir { + path.join(format!("{}.wit.print", &pkg.name.name)) + } else { + path.with_extension("wit.print") + }; + assert_output(&expected, &output)?; + } UnresolvedPackage::parse("foo.wit".as_ref(), &output) .context("failed to parse printed output")?; diff --git a/crates/wit-component/tests/linking.rs b/crates/wit-component/tests/linking.rs index b774827dc1..acb3606948 100644 --- a/crates/wit-component/tests/linking.rs +++ b/crates/wit-component/tests/linking.rs @@ -141,7 +141,7 @@ fn encode(wat: &str, wit: Option<&str>) -> Result> { if let Some(wit) = wit { let mut resolve = Resolve::default(); - let pkg = resolve.push(UnresolvedPackage::parse(Path::new("wit"), wit)?)?; + let pkg = resolve.push(UnresolvedPackage::parse(Path::new("wit"), wit)?.remove(0))?; let world = resolve.select_world(pkg, None)?; wit_component::embed_component_metadata( diff --git a/crates/wit-component/tests/merge.rs b/crates/wit-component/tests/merge.rs index 264c0c0d59..7620232282 100644 --- a/crates/wit-component/tests/merge.rs +++ b/crates/wit-component/tests/merge.rs @@ -46,7 +46,7 @@ fn merging() -> Result<()> { .join("merge") .join(&pkg.name.name) .with_extension("wit"); - let output = WitPrinter::default().print(&into, id)?; + let output = WitPrinter::default().print(&into, vec![id])?; assert_output(&expected, &output)?; } } diff --git a/crates/wit-component/tests/targets.rs b/crates/wit-component/tests/targets.rs index 23579b7f50..a3cf28965f 100644 --- a/crates/wit-component/tests/targets.rs +++ b/crates/wit-component/tests/targets.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use std::{fs, path::Path}; use wit_parser::{Resolve, UnresolvedPackage, WorldId}; @@ -79,8 +79,15 @@ 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 package = + let mut packages = UnresolvedPackage::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 package = packages.remove(0); let mut resolve = Resolve::default(); let package_id = resolve.push(package)?; diff --git a/crates/wit-component/tests/wit.rs b/crates/wit-component/tests/wit.rs index 92d7713288..cb0dc648e9 100644 --- a/crates/wit-component/tests/wit.rs +++ b/crates/wit-component/tests/wit.rs @@ -9,7 +9,7 @@ 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; + let package_id = resolver.push_path("tests/wit/parse-dir/wit")?[0].0; assert!(resolver .select_world(package_id, "foo-world".into()) .is_ok()); @@ -23,9 +23,7 @@ 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; + 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())?; assert!(resolver .interfaces diff --git a/crates/wit-parser/src/ast.rs b/crates/wit-parser/src/ast.rs index 05bd50cf0b..b094e5533b 100644 --- a/crates/wit-parser/src/ast.rs +++ b/crates/wit-parser/src/ast.rs @@ -3,6 +3,7 @@ use anyhow::{bail, Context, Result}; use lex::{Span, Token, Tokenizer}; use semver::Version; use std::borrow::Cow; +use std::collections::HashSet; use std::fmt; use std::path::{Path, PathBuf}; @@ -14,29 +15,129 @@ pub mod toposort; pub use lex::validate_id; -pub struct Ast<'a> { +enum Ast<'a> { + ExplicitPackages(Vec>), + PartialImplicitPackage(PartialImplicitPackage<'a>), +} + +pub struct ExplicitPackage<'a> { + package_id: PackageName<'a>, + decl_list: DeclList<'a>, +} + +pub struct PartialImplicitPackage<'a> { package_id: Option>, + decl_list: DeclList<'a>, +} + +/// Stores all of the declarations in a package's scope. In AST terms, this +/// means everything except the `package` declaration that demarcates a package +/// scope. In the traditional implicit format, these are all of the declarations +/// non-`package` declarations in the file: +/// +/// ```wit +/// package foo:name; +/// +/// /* START DECL LIST */ +/// // Some comment... +/// interface i {} +/// world w {} +/// /* END DECL LIST */ +/// ``` +/// +/// In the explicit package style, a [`DeclList`] is everything inside of each +/// `package` element's brackets: +/// +/// ```wit +/// package foo:name { +/// /* START FIRST DECL LIST */ +/// // Some comment... +/// interface i {} +/// world w {} +/// /* END FIRST DECL LIST */ +/// } +/// +/// package bar:name { +/// /* START SECOND DECL LIST */ +/// // Some comment... +/// interface i {} +/// world w {} +/// /* END SECOND DECL LIST */ +/// } +/// ``` +pub struct DeclList<'a> { items: Vec>, } +fn parse_explicit_package_items<'a>(tokens: &mut Tokenizer<'a>) -> Result> { + let mut items = Vec::new(); + let mut docs = parse_docs(tokens)?; + loop { + if tokens.eat(Token::RightBrace)? { + return Ok(DeclList { items }); + } + items.push(AstItem::parse(tokens, docs)?); + docs = parse_docs(tokens)?; + } +} + +fn parse_implicit_package_items<'a>( + tokens: &mut Tokenizer<'a>, + mut docs: Docs<'a>, +) -> Result> { + let mut items = Vec::new(); + while tokens.clone().next()?.is_some() { + items.push(AstItem::parse(tokens, docs)?); + docs = parse_docs(tokens)?; + } + Ok(DeclList { items }) +} + impl<'a> Ast<'a> { - pub fn parse(lexer: &mut Tokenizer<'a>) -> Result { - let mut items = Vec::new(); - let mut package_id = None; - let mut docs = parse_docs(lexer)?; - if lexer.eat(Token::Package)? { - let package_docs = std::mem::take(&mut docs); - package_id = Some(PackageName::parse(lexer, package_docs)?); - lexer.expect_semicolon()?; - docs = parse_docs(lexer)?; + fn parse(tokens: &mut Tokenizer<'a>) -> Result { + let mut maybe_package_id = None; + let mut packages = Vec::new(); + let mut docs = parse_docs(tokens)?; + loop { + if tokens.clone().next()?.is_none() { + break; + } + if !tokens.eat(Token::Package)? { + if !packages.is_empty() { + bail!("WIT files cannot mix top-level explicit `package` declarations with other declaration kinds"); + } + break; + } + + let package_id = PackageName::parse(tokens, std::mem::take(&mut docs))?; + if tokens.eat(Token::LeftBrace)? { + packages.push(ExplicitPackage { + package_id: package_id, + decl_list: parse_explicit_package_items(tokens)?, + }); + docs = parse_docs(tokens)?; + } else { + maybe_package_id = Some(package_id); + tokens.expect_semicolon()?; + if !packages.is_empty() { + bail!("WIT files cannot mix top-level explicit `package` declarations with other declaration kinds"); + } + docs = parse_docs(tokens)?; + break; + } } - while lexer.clone().next()?.is_some() { - items.push(AstItem::parse(lexer, docs)?); - docs = parse_docs(lexer)?; + + if packages.is_empty() { + return Ok(Ast::PartialImplicitPackage(PartialImplicitPackage { + package_id: maybe_package_id, + decl_list: parse_implicit_package_items(tokens, docs)?, + })); } - Ok(Self { package_id, items }) + Ok(Ast::ExplicitPackages(packages)) } +} +impl<'a> DeclList<'a> { fn for_each_path<'b>( &'b self, mut f: impl FnMut( @@ -1529,10 +1630,13 @@ impl SourceMap { self.offset = new_offset; } - /// Parses the files added to this source map into an [`UnresolvedPackage`]. - pub fn parse(self) -> Result { - let mut doc = self.rewrite_error(|| { - let mut resolver = Resolver::default(); + /// Parses the files added to this source map into one or more [`UnresolvedPackage`]s. + pub fn parse(self) -> Result> { + let mut implicit_pkg: Option = None; + let explicit_pkgs = self.rewrite_error(|| { + let mut ret_packages = Vec::new(); + let mut explicit_pkg_names: HashSet = HashSet::new(); + let mut implicit_resolver = Resolver::default(); let mut srcs = self.sources.iter().collect::>(); srcs.sort_by_key(|src| &src.path); for src in srcs { @@ -1543,15 +1647,58 @@ impl SourceMap { self.require_f32_f64, ) .with_context(|| format!("failed to tokenize path: {}", src.path.display()))?; - let ast = Ast::parse(&mut tokens)?; - resolver.push(ast).with_context(|| { - format!("failed to start resolving path: {}", src.path.display()) - })?; + match Ast::parse(&mut tokens)? { + Ast::ExplicitPackages(pkgs) => { + for pkg in pkgs { + let mut resolver = Resolver::default(); + let pkg_name = pkg.package_id.package_name(); + let ingested = resolver.push_then_resolve(pkg).with_context(|| { + format!("failed to start resolving path: {}", src.path.display()) + })?; + + match explicit_pkg_names.get(&pkg_name) { + Some(_) => bail!( + "colliding explicit package names, multiple packages named `{}`", + pkg_name + ), + None => explicit_pkg_names.insert(pkg_name), + }; + + if let Some(unresolved) = ingested { + ret_packages.push(unresolved); + } + } + } + Ast::PartialImplicitPackage(partial) => { + if !explicit_pkg_names.is_empty() { + bail!("WIT files cannot mix top-level explicit `package` declarations with other declaration kinds 2"); + } + + implicit_resolver.push_partial(partial).with_context(|| { + format!("failed to start resolving path: {}", src.path.display()) + })?; + } + } + } + + if let Some(pkg) = implicit_resolver.resolve()? { + implicit_pkg = Some(pkg) } - resolver.resolve() + Ok(ret_packages) })?; - doc.source_map = self; - Ok(doc) + + match implicit_pkg { + Some(mut pkg) => match explicit_pkgs.is_empty() { + true => { + pkg.source_map = self; + Ok(vec![pkg]) + } + false => { + bail!("WIT files cannot mix top-level explicit `package` declarations with other declaration kinds"); + } + }, + None => Ok(explicit_pkgs), + } } pub(crate) fn rewrite_error(&self, f: F) -> Result @@ -1648,21 +1795,21 @@ impl SourceMap { } } -pub(crate) enum AstUsePath { +pub enum ParsedUsePath { Name(String), Package(crate::PackageName, String), } -pub(crate) fn parse_use_path(s: &str) -> Result { +pub fn parse_use_path(s: &str) -> Result { let mut tokens = Tokenizer::new(s, 0, Some(true), None)?; let path = UsePath::parse(&mut tokens)?; if tokens.next()?.is_some() { bail!("trailing tokens in path specifier"); } Ok(match path { - UsePath::Id(id) => AstUsePath::Name(id.name.to_string()), + UsePath::Id(id) => ParsedUsePath::Name(id.name.to_string()), UsePath::Package { id, name } => { - AstUsePath::Package(id.package_name(), name.name.to_string()) + ParsedUsePath::Package(id.package_name(), name.name.to_string()) } }) } diff --git a/crates/wit-parser/src/ast/lex.rs b/crates/wit-parser/src/ast/lex.rs index e7a45fcf75..c81aa5080a 100644 --- a/crates/wit-parser/src/ast/lex.rs +++ b/crates/wit-parser/src/ast/lex.rs @@ -194,6 +194,9 @@ impl<'a> Tokenizer<'a> { } } + /// Three possibilities when calling this method: an `Err(...)` indicates that lexing failed, an + /// `Ok(Some(...))` produces the next token, and `Ok(None)` indicates that there are no more + /// tokens available. pub fn next_raw(&mut self) -> Result, Error> { let (str_start, ch) = match self.chars.next() { Some(pair) => pair, diff --git a/crates/wit-parser/src/ast/resolve.rs b/crates/wit-parser/src/ast/resolve.rs index 4004a84635..6ff02dd33a 100644 --- a/crates/wit-parser/src/ast/resolve.rs +++ b/crates/wit-parser/src/ast/resolve.rs @@ -13,8 +13,8 @@ pub struct Resolver<'a> { /// Package docs. package_docs: Docs, - /// All WIT files which are going to be resolved together. - asts: Vec>, + /// All non-`package` WIT decls are going to be resolved together. + decl_lists: Vec>, // Arenas that get plumbed to the final `UnresolvedPackage` types: Arena, @@ -29,9 +29,9 @@ pub struct Resolver<'a> { /// is updated as the ASTs are walked. cur_ast_index: usize, - /// A map per `ast::Ast` which keeps track of the file's top level names in - /// scope. This maps each name onto either a world or an interface, handling - /// things like `use` at the top level. + /// A map per `ast::DeclList` which keeps track of the file's top level + /// names in scope. This maps each name onto either a world or an interface, + /// handling things like `use` at the top level. ast_items: Vec>, /// A map for the entire package being created of all names defined within, @@ -109,11 +109,11 @@ enum TypeOrItem { } impl<'a> Resolver<'a> { - pub(crate) fn push(&mut self, ast: ast::Ast<'a>) -> Result<()> { + pub(crate) fn push_partial(&mut self, partial: ast::PartialImplicitPackage<'a>) -> Result<()> { // As each WIT file is pushed into this resolver keep track of the // current package name assigned. Only one file needs to mention it, but // if multiple mention it then they must all match. - if let Some(cur) = &ast.package_id { + if let Some(cur) = &partial.package_id { let cur_name = cur.package_name(); if let Some(prev) = &self.package_name { if cur_name != *prev { @@ -140,33 +140,38 @@ impl<'a> Resolver<'a> { self.package_docs = docs; } } - self.asts.push(ast); + self.decl_lists.push(partial.decl_list); Ok(()) } - pub(crate) fn resolve(&mut self) -> Result { + pub(crate) fn resolve(&mut self) -> Result> { // At least one of the WIT files must have a `package` annotation. let name = match &self.package_name { Some(name) => name.clone(), - None => bail!("no `package` header was found in any WIT file for this package"), + None => { + if self.decl_lists.is_empty() { + return Ok(None); + } + bail!("no `package` header was found in any WIT file for this package") + } }; // First populate information about foreign dependencies and the general // structure of the package. This should resolve the "base" of many // `use` statements and additionally generate a topological ordering of // all interfaces in the package to visit. - let asts = mem::take(&mut self.asts); - self.populate_foreign_deps(&asts); - let (iface_order, world_order) = self.populate_ast_items(&asts)?; - self.populate_foreign_types(&asts)?; + let decl_lists = mem::take(&mut self.decl_lists); + self.populate_foreign_deps(&decl_lists); + let (iface_order, world_order) = self.populate_ast_items(&decl_lists)?; + self.populate_foreign_types(&decl_lists)?; // Use the topological ordering of all interfaces to resolve all // interfaces in-order. Note that a reverse-mapping from ID to AST is // generated here to assist with this. let mut iface_id_to_ast = IndexMap::new(); let mut world_id_to_ast = IndexMap::new(); - for (i, ast) in asts.iter().enumerate() { - for item in ast.items.iter() { + for (i, decl_list) in decl_lists.iter().enumerate() { + for item in decl_list.items.iter() { match item { ast::AstItem::Interface(iface) => { let id = match self.ast_items[i][iface.name.name] { @@ -199,7 +204,7 @@ impl<'a> Resolver<'a> { self.resolve_world(id, world)?; } - Ok(UnresolvedPackage { + Ok(Some(UnresolvedPackage { name, docs: mem::take(&mut self.package_docs), worlds: mem::take(&mut self.worlds), @@ -224,59 +229,70 @@ impl<'a> Resolver<'a> { foreign_dep_spans: mem::take(&mut self.foreign_dep_spans), source_map: SourceMap::default(), required_resource_types: mem::take(&mut self.required_resource_types), - }) + })) + } + + pub(crate) fn push_then_resolve( + &mut self, + package: ast::ExplicitPackage<'a>, + ) -> Result> { + self.package_name = Some(package.package_id.package_name()); + self.docs(&package.package_id.docs); + self.decl_lists = vec![package.decl_list]; + self.resolve() } /// Registers all foreign dependencies made within the ASTs provided. /// /// This will populate the `self.foreign_{deps,interfaces,worlds}` maps with all /// `UsePath::Package` entries. - fn populate_foreign_deps(&mut self, asts: &[ast::Ast<'a>]) { + fn populate_foreign_deps(&mut self, decl_lists: &[ast::DeclList<'a>]) { let mut foreign_deps = mem::take(&mut self.foreign_deps); let mut foreign_interfaces = mem::take(&mut self.foreign_interfaces); let mut foreign_worlds = mem::take(&mut self.foreign_worlds); - for ast in asts { - ast.for_each_path(|_, path, _names, world_or_iface| { - let (id, name) = match path { - ast::UsePath::Package { id, name } => (id, name), - _ => return Ok(()), - }; - - let deps = foreign_deps.entry(id.package_name()).or_insert_with(|| { - self.foreign_dep_spans.push(id.span); - IndexMap::new() - }); - let id = *deps.entry(name.name).or_insert_with(|| { - match world_or_iface { - WorldOrInterface::World => { - log::trace!( - "creating a world for foreign dep: {}/{}", - id.package_name(), - name.name - ); - AstItem::World(self.alloc_world(name.span)) - } - WorldOrInterface::Interface | WorldOrInterface::Unknown => { - // Currently top-level `use` always assumes an interface, so the - // `Unknown` case is the same as `Interface`. - log::trace!( - "creating an interface for foreign dep: {}/{}", - id.package_name(), - name.name - ); - AstItem::Interface(self.alloc_interface(name.span)) + for decl_list in decl_lists { + decl_list + .for_each_path(|_, path, _names, world_or_iface| { + let (id, name) = match path { + ast::UsePath::Package { id, name } => (id, name), + _ => return Ok(()), + }; + + let deps = foreign_deps.entry(id.package_name()).or_insert_with(|| { + self.foreign_dep_spans.push(id.span); + IndexMap::new() + }); + let id = *deps.entry(name.name).or_insert_with(|| { + match world_or_iface { + WorldOrInterface::World => { + log::trace!( + "creating a world for foreign dep: {}/{}", + id.package_name(), + name.name + ); + AstItem::World(self.alloc_world(name.span)) + } + WorldOrInterface::Interface | WorldOrInterface::Unknown => { + // Currently top-level `use` always assumes an interface, so the + // `Unknown` case is the same as `Interface`. + log::trace!( + "creating an interface for foreign dep: {}/{}", + id.package_name(), + name.name + ); + AstItem::Interface(self.alloc_interface(name.span)) + } } - } - }); + }); - let _ = match id { - AstItem::Interface(id) => foreign_interfaces.insert(id), - AstItem::World(id) => foreign_worlds.insert(id), - }; + let _ = match id { + AstItem::Interface(id) => foreign_interfaces.insert(id), + AstItem::World(id) => foreign_worlds.insert(id), + }; - Ok(()) - }) - .unwrap(); + Ok(()) + }) + .unwrap(); } self.foreign_deps = foreign_deps; self.foreign_interfaces = foreign_interfaces; @@ -323,18 +339,18 @@ impl<'a> Resolver<'a> { /// generated for resolving use-paths later on. fn populate_ast_items( &mut self, - asts: &[ast::Ast<'a>], + decl_lists: &[ast::DeclList<'a>], ) -> Result<(Vec, Vec)> { let mut package_items = IndexMap::new(); // Validate that all worlds and interfaces have unique names within this // package across all ASTs which make up the package. let mut names = HashMap::new(); - let mut ast_namespaces = Vec::new(); + let mut decl_list_namespaces = Vec::new(); let mut order = IndexMap::new(); - for ast in asts { - let mut ast_ns = IndexMap::new(); - for item in ast.items.iter() { + for decl_list in decl_lists { + let mut decl_list_ns = IndexMap::new(); + for item in decl_list.items.iter() { match item { ast::AstItem::Interface(i) => { if package_items.insert(i.name.name, i.name.span).is_some() { @@ -343,7 +359,7 @@ impl<'a> Resolver<'a> { format!("duplicate item named `{}`", i.name.name), )) } - let prev = ast_ns.insert(i.name.name, ()); + let prev = decl_list_ns.insert(i.name.name, ()); assert!(prev.is_none()); let prev = order.insert(i.name.name, Vec::new()); assert!(prev.is_none()); @@ -357,7 +373,7 @@ impl<'a> Resolver<'a> { format!("duplicate item named `{}`", w.name.name), )) } - let prev = ast_ns.insert(w.name.name, ()); + let prev = decl_list_ns.insert(w.name.name, ()); assert!(prev.is_none()); let prev = order.insert(w.name.name, Vec::new()); assert!(prev.is_none()); @@ -368,7 +384,7 @@ impl<'a> Resolver<'a> { ast::AstItem::Use(_) => {} } } - ast_namespaces.push(ast_ns); + decl_list_namespaces.push(decl_list_ns); } // Next record dependencies between interfaces as induced via `use` @@ -380,13 +396,13 @@ impl<'a> Resolver<'a> { Local(ast::Id<'a>), } - for ast in asts { + for decl_list in decl_lists { // Record, in the context of this file, what all names are defined // at the top level and whether they point to other items in this // package or foreign items. Foreign deps are ignored for // topological ordering. - let mut ast_ns = IndexMap::new(); - for item in ast.items.iter() { + let mut decl_list_ns = IndexMap::new(); + for item in decl_list.items.iter() { let (name, src) = match item { ast::AstItem::Use(u) => { let name = u.as_.as_ref().unwrap_or(u.item.name()); @@ -399,7 +415,7 @@ impl<'a> Resolver<'a> { ast::AstItem::Interface(i) => (&i.name, ItemSource::Local(i.name.clone())), ast::AstItem::World(w) => (&w.name, ItemSource::Local(w.name.clone())), }; - if ast_ns.insert(name.name, (name.span, src)).is_some() { + if decl_list_ns.insert(name.name, (name.span, src)).is_some() { bail!(Error::new( name.span, format!("duplicate name `{}` in this file", name.name), @@ -409,7 +425,7 @@ impl<'a> Resolver<'a> { // With this file's namespace information look at all `use` paths // and record dependencies between interfaces. - ast.for_each_path(|iface, path, _names, _| { + decl_list.for_each_path(|iface, path, _names, _| { // If this import isn't contained within an interface then it's // in a world and it doesn't need to participate in our // topo-sort. @@ -421,7 +437,7 @@ impl<'a> Resolver<'a> { ast::UsePath::Id(id) => id, ast::UsePath::Package { .. } => return Ok(()), }; - match ast_ns.get(used_name.name) { + match decl_list_ns.get(used_name.name) { Some((_, ItemSource::Foreign)) => return Ok(()), Some((_, ItemSource::Local(id))) => { order[iface.name].push(id.clone()); @@ -473,9 +489,9 @@ impl<'a> Resolver<'a> { ast::AstItem::Use(_) => unreachable!(), }; } - for ast in asts { + for decl_list in decl_lists { let mut items = IndexMap::new(); - for item in ast.items.iter() { + for item in decl_list.items.iter() { let (name, ast_item) = match item { ast::AstItem::Use(u) => { if !u.attributes.is_empty() { @@ -533,10 +549,10 @@ impl<'a> Resolver<'a> { /// This is done after all interfaces are generated so `self.resolve_path` /// can be used to determine if what's being imported from is a foreign /// interface or not. - fn populate_foreign_types(&mut self, asts: &[ast::Ast<'a>]) -> Result<()> { - for (i, ast) in asts.iter().enumerate() { + fn populate_foreign_types(&mut self, decl_lists: &[ast::DeclList<'a>]) -> Result<()> { + for (i, decl_list) in decl_lists.iter().enumerate() { self.cur_ast_index = i; - ast.for_each_path(|_, path, names, _| { + decl_list.for_each_path(|_, path, names, _| { let names = match names { Some(names) => names, None => return Ok(()), diff --git a/crates/wit-parser/src/decoding.rs b/crates/wit-parser/src/decoding.rs index 848aa6ea66..2281908a82 100644 --- a/crates/wit-parser/src/decoding.rs +++ b/crates/wit-parser/src/decoding.rs @@ -344,12 +344,12 @@ impl ComponentInfo { /// Result of the [`decode`] function. pub enum DecodedWasm { - /// The input to [`decode`] was a binary-encoded WIT package. + /// The input to [`decode`] was one or more binary-encoded WIT package(s). /// - /// The full resolve graph is here plus the identifier of the package that - /// was encoded. Note that other packages may be within the resolve if this - /// package refers to foreign packages. - WitPackage(Resolve, PackageId), + /// The full resolve graph is here plus the identifier of the packages that + /// were encoded. Note that other packages may be within the resolve if any + /// of the main packages refer to other, foreign packages. + WitPackages(Resolve, Vec), /// The input to [`decode`] was a component and its interface is specified /// by the world here. @@ -360,16 +360,16 @@ impl DecodedWasm { /// Returns the [`Resolve`] for WIT types contained. pub fn resolve(&self) -> &Resolve { match self { - DecodedWasm::WitPackage(resolve, _) => resolve, + DecodedWasm::WitPackages(resolve, _) => resolve, DecodedWasm::Component(resolve, _) => resolve, } } - /// Returns the main package of what was decoded. - pub fn package(&self) -> PackageId { + /// Returns the main packages of what was decoded. + pub fn packages(&self) -> Vec { match self { - DecodedWasm::WitPackage(_, id) => *id, - DecodedWasm::Component(resolve, world) => resolve.worlds[*world].package.unwrap(), + DecodedWasm::WitPackages(_, ids) => ids.clone(), + DecodedWasm::Component(resolve, world) => vec![resolve.worlds[*world].package.unwrap()], } } } @@ -383,12 +383,12 @@ pub fn decode_reader(reader: impl Read) -> Result { WitEncodingVersion::V1 => { log::debug!("decoding a v1 WIT package encoded as wasm"); let (resolve, pkg) = info.decode_wit_v1_package()?; - Ok(DecodedWasm::WitPackage(resolve, pkg)) + Ok(DecodedWasm::WitPackages(resolve, vec![pkg])) } WitEncodingVersion::V2 => { log::debug!("decoding a v2 WIT package encoded as wasm"); let (resolve, pkg) = info.decode_wit_v2_package()?; - Ok(DecodedWasm::WitPackage(resolve, pkg)) + Ok(DecodedWasm::WitPackages(resolve, vec![pkg])) } } } else { diff --git a/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs index a9f54bd13c..610cb85468 100644 --- a/crates/wit-parser/src/lib.rs +++ b/crates/wit-parser/src/lib.rs @@ -17,6 +17,7 @@ pub mod abi; mod ast; use ast::lex::Span; pub use ast::SourceMap; +pub use ast::{parse_use_path, ParsedUsePath}; mod sizealign; pub use sizealign::*; mod resolve; @@ -214,7 +215,7 @@ impl UnresolvedPackage { /// /// 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 { + pub fn parse(path: &Path, contents: &str) -> Result> { let mut map = SourceMap::default(); map.push(path, contents); map.parse() @@ -225,7 +226,7 @@ impl UnresolvedPackage { /// The path provided is inferred whether it's a file or a directory. A file /// is parsed with [`UnresolvedPackage::parse_file`] and a directory is /// parsed with [`UnresolvedPackage::parse_dir`]. - pub fn parse_path(path: &Path) -> Result { + pub fn parse_path(path: &Path) -> Result> { if path.is_dir() { UnresolvedPackage::parse_dir(path) } else { @@ -237,7 +238,7 @@ impl UnresolvedPackage { /// /// 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 { + pub fn parse_file(path: &Path) -> Result> { let contents = std::fs::read_to_string(path) .with_context(|| format!("failed to read file {path:?}"))?; Self::parse(path, &contents) @@ -247,7 +248,7 @@ impl UnresolvedPackage { /// /// 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 { + pub fn parse_dir(path: &Path) -> Result> { let mut map = SourceMap::default(); let cx = || format!("failed to read directory {path:?}"); for entry in path.read_dir().with_context(&cx)? { diff --git a/crates/wit-parser/src/resolve.rs b/crates/wit-parser/src/resolve.rs index fa294f986d..5e95187af9 100644 --- a/crates/wit-parser/src/resolve.rs +++ b/crates/wit-parser/src/resolve.rs @@ -1,5 +1,5 @@ use crate::ast::lex::Span; -use crate::ast::{parse_use_path, AstUsePath}; +use crate::ast::{parse_use_path, ParsedUsePath}; #[cfg(feature = "serde")] use crate::serde_::{serialize_arena, serialize_id_map}; use crate::{ @@ -105,7 +105,33 @@ pub type PackageId = Id; enum ParsedFile { #[cfg(feature = "decoding")] Package(PackageId), - Unresolved(UnresolvedPackage), + Unresolved(Vec), +} + +/// Visitor helper for performing topological sort on a group of packages. +fn visit<'a>( + pkg: &'a UnresolvedPackage, + deps: &'a BTreeMap, + order: &mut IndexSet, + visiting: &mut HashSet<&'a PackageName>, +) -> Result<()> { + if order.contains(&pkg.name) { + return Ok(()); + } + pkg.source_map.rewrite_error(|| { + for (i, (dep, _)) in pkg.foreign_deps.iter().enumerate() { + let span = pkg.foreign_dep_spans[i]; + if !visiting.insert(dep) { + bail!(Error::new(span, "package depends on itself")); + } + if let Some(dep) = deps.get(dep) { + visit(dep, deps, order, visiting)?; + } + assert!(visiting.remove(dep)); + } + assert!(order.insert(pkg.name.clone())); + Ok(()) + }) } impl Resolve { @@ -131,11 +157,11 @@ impl Resolve { /// /// Returns the top-level [`PackageId`] as well as a list of all files read /// during this parse. - pub fn push_path(&mut self, path: impl AsRef) -> Result<(PackageId, Vec)> { + pub fn push_path(&mut self, path: impl AsRef) -> Result)>> { self._push_path(path.as_ref()) } - fn _push_path(&mut self, path: &Path) -> Result<(PackageId, Vec)> { + fn _push_path(&mut self, path: &Path) -> Result)>> { if path.is_dir() { self.push_dir(path).with_context(|| { format!( @@ -144,8 +170,12 @@ impl Resolve { ) }) } else { - let id = self.push_file(path)?; - Ok((id, vec![path.to_path_buf()])) + let ids = self.push_file(path)?; + let mut ret = Vec::new(); + for id in ids { + ret.push((id, vec![path.to_path_buf()])); + } + Ok(ret) } } @@ -167,66 +197,48 @@ impl Resolve { /// /// This function returns the [`PackageId`] of the root parsed package at /// `path`, along with a list of all paths that were consumed during parsing - /// for the root package and all dependency packages. - pub fn push_dir(&mut self, path: &Path) -> Result<(PackageId, Vec)> { - let pkg = UnresolvedPackage::parse_dir(path) + /// for the root package and all dependency packages, for each package encountered. + pub fn push_dir(&mut self, path: &Path) -> Result)>> { + let mut ret = Vec::new(); + let pkgs = UnresolvedPackage::parse_dir(path) .with_context(|| format!("failed to parse package: {}", path.display()))?; - let deps = path.join("deps"); - let mut deps = self - .parse_deps_dir(&deps) - .with_context(|| format!("failed to parse dependency directory: {}", deps.display()))?; - - // Perform a simple topological sort which will bail out on cycles - // and otherwise determine the order that packages must be added to - // this `Resolve`. - let mut order = IndexSet::new(); - let mut visiting = HashSet::new(); - for pkg in deps.values().chain([&pkg]) { - visit(&pkg, &deps, &mut order, &mut visiting)?; - } - - // Using the topological ordering insert each package incrementally. - // Additionally note that the last item visited here is the root - // package, which is the one returned here. - let mut last = None; - let mut files = Vec::new(); - let mut pkg = Some(pkg); - for name in order { - let pkg = deps.remove(&name).unwrap_or_else(|| pkg.take().unwrap()); - files.extend(pkg.source_files().map(|p| p.to_path_buf())); - let pkgid = self.push(pkg)?; - last = Some(pkgid); - } - - return Ok((last.unwrap(), files)); - - fn visit<'a>( - pkg: &'a UnresolvedPackage, - deps: &'a BTreeMap, - order: &mut IndexSet, - visiting: &mut HashSet<&'a PackageName>, - ) -> Result<()> { - if order.contains(&pkg.name) { - return Ok(()); - } - pkg.source_map.rewrite_error(|| { - for (i, (dep, _)) in pkg.foreign_deps.iter().enumerate() { - let span = pkg.foreign_dep_spans[i]; - if !visiting.insert(dep) { - bail!(Error::new(span, "package depends on itself")); - } - if let Some(dep) = deps.get(dep) { - visit(dep, deps, order, visiting)?; - } - assert!(visiting.remove(dep)); - } - assert!(order.insert(pkg.name.clone())); - Ok(()) - }) + for pkg in pkgs { + let deps = path.join("deps"); + let mut deps = self.parse_deps_dir(&deps).with_context(|| { + format!("failed to parse dependency directory: {}", deps.display()) + })?; + + // Perform a simple topological sort which will bail out on cycles + // and otherwise determine the order that packages must be added to + // this `Resolve`. + let mut order = IndexSet::new(); + let mut visiting = HashSet::new(); + for dep_pkg in deps.values().chain([&pkg]) { + visit(&dep_pkg, &deps, &mut order, &mut visiting)?; + } + + // Using the topological ordering insert each package incrementally. + // Additionally note that the last item visited here is the root + // package, which is the one returned here. + let mut last = None; + let mut files = Vec::new(); + let mut pkg = Some(pkg); + for name in order { + let pkg = deps.remove(&name).unwrap_or_else(|| pkg.take().unwrap()); + files.extend(pkg.source_files().map(|p| p.to_path_buf())); + let pkgid = self.push(pkg)?; + last = Some(pkgid); + } + + ret.push((last.unwrap(), files)); } + + Ok(ret) } + // TODO: this is a pretty naive implementation, since for example it doesn't re-use dependencies + // between "multi-"packages. Could maybe do better going forward? fn parse_deps_dir(&mut self, path: &Path) -> Result> { let mut ret = BTreeMap::new(); // If there's no `deps` dir, then there's no deps, so return the @@ -241,8 +253,7 @@ impl Resolve { entries.sort_by_key(|e| e.file_name()); for dep in entries { let path = dep.path(); - - let pkg = if dep.file_type()?.is_dir() || path.metadata()?.is_dir() { + let pkgs = if dep.file_type()?.is_dir() || path.metadata()?.is_dir() { // If this entry is a directory or a symlink point to a // directory then always parse it as an `UnresolvedPackage` // since it's intentional to not support recursive `deps` @@ -258,7 +269,7 @@ impl Resolve { Some("wit") | Some("wat") | Some("wasm") => match self._push_file(&path)? { #[cfg(feature = "decoding")] ParsedFile::Package(_) => continue, - ParsedFile::Unresolved(pkg) => pkg, + ParsedFile::Unresolved(pkgs) => pkgs, }, // Other files in deps dir are ignored for now to avoid @@ -267,9 +278,9 @@ impl Resolve { _ => continue, } }; - let prev = ret.insert(pkg.name.clone(), pkg); - if let Some(prev) = prev { - bail!("duplicate definitions of package `{}` found", prev.name); + + for pkg in pkgs { + ret.insert(pkg.name.clone(), pkg); } } Ok(ret) @@ -288,11 +299,11 @@ impl Resolve { /// /// In both situations the `PackageId` of the resulting resolved package is /// returned from this method. - pub fn push_file(&mut self, path: impl AsRef) -> Result { + pub fn push_file(&mut self, path: impl AsRef) -> Result> { match self._push_file(path.as_ref())? { #[cfg(feature = "decoding")] - ParsedFile::Package(id) => Ok(id), - ParsedFile::Unresolved(pkg) => self.push(pkg), + ParsedFile::Package(id) => Ok(vec![id]), + ParsedFile::Unresolved(pkgs) => self.append(pkgs), } } @@ -322,9 +333,9 @@ impl Resolve { DecodedWasm::Component(..) => { bail!("found an actual component instead of an encoded WIT package in wasm") } - DecodedWasm::WitPackage(resolve, pkg) => { + DecodedWasm::WitPackages(resolve, pkgs) => { let remap = self.merge(resolve)?; - return Ok(ParsedFile::Package(remap.packages[pkg.index()])); + return Ok(ParsedFile::Package(remap.packages[pkgs[0].index()])); } } } @@ -335,8 +346,8 @@ impl Resolve { Ok(s) => s, Err(_) => bail!("input file is not valid utf-8 [{}]", path.display()), }; - let pkg = UnresolvedPackage::parse(path, text)?; - Ok(ParsedFile::Unresolved(pkg)) + let pkgs = UnresolvedPackage::parse(path, text)?; + Ok(ParsedFile::Unresolved(pkgs)) } /// Appends a new [`UnresolvedPackage`] to this [`Resolve`], creating a @@ -347,13 +358,31 @@ impl Resolve { /// as [`Resolve::push_path`]. /// /// Any dependency resolution error or otherwise world-elaboration error - /// will be returned here. If successful a package identifier is returned + /// will be returned here, if successful a package identifier is returned /// which corresponds to the package that was just inserted. pub fn push(&mut self, mut unresolved: UnresolvedPackage) -> Result { let source_map = mem::take(&mut unresolved.source_map); source_map.rewrite_error(|| Remap::default().append(self, unresolved)) } + /// Appends new [`UnresolvedPackage`]s 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. + pub fn append(&mut self, unresolved: Vec) -> Result> { + let mut ids = Vec::new(); + for mut pkg in unresolved { + let source_map = mem::take(&mut pkg.source_map); + ids.push(source_map.rewrite_error(|| Remap::default().append(self, pkg))?); + } + Ok(ids) + } + pub fn all_bits_valid(&self, ty: &Type) -> bool { match ty { Type::U8 @@ -760,8 +789,8 @@ impl Resolve { let path = parse_use_path(world) .with_context(|| format!("failed to parse world specifier `{world}`"))?; let (pkg, world) = match path { - AstUsePath::Name(name) => (pkg, name), - AstUsePath::Package(pkg, interface) => { + ParsedUsePath::Name(name) => (pkg, name), + ParsedUsePath::Package(pkg, interface) => { let pkg = match self.package_names.get(&pkg) { Some(pkg) => *pkg, None => { @@ -2492,7 +2521,7 @@ mod tests { } fn parse_into(resolve: &mut Resolve, wit: &str) -> PackageId { - let pkg = crate::UnresolvedPackage::parse("input.wit".as_ref(), wit).unwrap(); - resolve.push(pkg).unwrap() + let pkgs = crate::UnresolvedPackage::parse("input.wit".as_ref(), wit).unwrap(); + resolve.append(pkgs).unwrap()[0] } } diff --git a/crates/wit-parser/tests/ui/multi-file-multi-package.wit.json b/crates/wit-parser/tests/ui/multi-file-multi-package.wit.json new file mode 100644 index 0000000000..3804061d01 --- /dev/null +++ b/crates/wit-parser/tests/ui/multi-file-multi-package.wit.json @@ -0,0 +1,250 @@ +{ + "worlds": [ + { + "name": "w1", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + }, + "imp1": { + "interface": { + "id": 1 + } + } + }, + "exports": {}, + "package": 0 + }, + { + "name": "w2", + "imports": { + "interface-2": { + "interface": { + "id": 2 + } + }, + "imp2": { + "interface": { + "id": 3 + } + } + }, + "exports": {}, + "package": 1 + }, + { + "name": "w3", + "imports": { + "interface-4": { + "interface": { + "id": 4 + } + }, + "imp3": { + "interface": { + "id": 5 + } + } + }, + "exports": {}, + "package": 2 + }, + { + "name": "w4", + "imports": { + "interface-6": { + "interface": { + "id": 6 + } + }, + "imp4": { + "interface": { + "id": 7 + } + } + }, + "exports": {}, + "package": 3 + } + ], + "interfaces": [ + { + "name": "i1", + "types": { + "a": 0 + }, + "functions": {}, + "package": 0 + }, + { + "name": null, + "types": { + "a": 1 + }, + "functions": {}, + "package": 0 + }, + { + "name": "i2", + "types": { + "b": 2 + }, + "functions": {}, + "package": 1 + }, + { + "name": null, + "types": { + "b": 3 + }, + "functions": {}, + "package": 1 + }, + { + "name": "i3", + "types": { + "a": 4 + }, + "functions": {}, + "package": 2 + }, + { + "name": null, + "types": { + "a": 5 + }, + "functions": {}, + "package": 2 + }, + { + "name": "i4", + "types": { + "b": 6 + }, + "functions": {}, + "package": 3 + }, + { + "name": null, + "types": { + "b": 7 + }, + "functions": {}, + "package": 3 + } + ], + "types": [ + { + "name": "a", + "kind": { + "type": "u32" + }, + "owner": { + "interface": 0 + } + }, + { + "name": "a", + "kind": { + "type": 0 + }, + "owner": { + "interface": 1 + } + }, + { + "name": "b", + "kind": { + "type": "u32" + }, + "owner": { + "interface": 2 + } + }, + { + "name": "b", + "kind": { + "type": 2 + }, + "owner": { + "interface": 3 + } + }, + { + "name": "a", + "kind": { + "type": "u32" + }, + "owner": { + "interface": 4 + } + }, + { + "name": "a", + "kind": { + "type": 4 + }, + "owner": { + "interface": 5 + } + }, + { + "name": "b", + "kind": { + "type": "u32" + }, + "owner": { + "interface": 6 + } + }, + { + "name": "b", + "kind": { + "type": 6 + }, + "owner": { + "interface": 7 + } + } + ], + "packages": [ + { + "name": "foo:name", + "interfaces": { + "i1": 0 + }, + "worlds": { + "w1": 0 + } + }, + { + "name": "bar:name", + "interfaces": { + "i2": 2 + }, + "worlds": { + "w2": 1 + } + }, + { + "name": "baz:name", + "interfaces": { + "i3": 4 + }, + "worlds": { + "w3": 2 + } + }, + { + "name": "qux:name", + "interfaces": { + "i4": 6 + }, + "worlds": { + "w4": 3 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/multi-file-multi-package/a.wit b/crates/wit-parser/tests/ui/multi-file-multi-package/a.wit new file mode 100644 index 0000000000..79e983765e --- /dev/null +++ b/crates/wit-parser/tests/ui/multi-file-multi-package/a.wit @@ -0,0 +1,23 @@ +package foo:name { + interface i1 { + type a = u32; + } + + world w1 { + import imp1: interface { + use i1.{a}; + } + } +} + +package bar:name { + interface i2 { + type b = u32; + } + + world w2 { + import imp2: interface { + use i2.{b}; + } + } +} diff --git a/crates/wit-parser/tests/ui/multi-file-multi-package/b.wit b/crates/wit-parser/tests/ui/multi-file-multi-package/b.wit new file mode 100644 index 0000000000..0dcd1d5d1c --- /dev/null +++ b/crates/wit-parser/tests/ui/multi-file-multi-package/b.wit @@ -0,0 +1,23 @@ +package baz:name { + interface i3 { + type a = u32; + } + + world w3 { + import imp3: interface { + use i3.{a}; + } + } +} + +package qux:name { + interface i4 { + type b = u32; + } + + world w4 { + import imp4: interface { + use i4.{b}; + } + } +} diff --git a/crates/wit-parser/tests/ui/packages-explicit-colliding-decl-names.wit b/crates/wit-parser/tests/ui/packages-explicit-colliding-decl-names.wit new file mode 100644 index 0000000000..876012cfb9 --- /dev/null +++ b/crates/wit-parser/tests/ui/packages-explicit-colliding-decl-names.wit @@ -0,0 +1,23 @@ +package foo:name { + interface i { + type a = u32; + } + + world w { + import imp: interface { + use i.{a}; + } + } +} + +package bar:name { + interface i { + type a = u32; + } + + world w { + import imp: interface { + use i.{a}; + } + } +} diff --git a/crates/wit-parser/tests/ui/packages-explicit-colliding-decl-names.wit.json b/crates/wit-parser/tests/ui/packages-explicit-colliding-decl-names.wit.json new file mode 100644 index 0000000000..b5a9eab691 --- /dev/null +++ b/crates/wit-parser/tests/ui/packages-explicit-colliding-decl-names.wit.json @@ -0,0 +1,130 @@ +{ + "worlds": [ + { + "name": "w", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + }, + "imp": { + "interface": { + "id": 1 + } + } + }, + "exports": {}, + "package": 0 + }, + { + "name": "w", + "imports": { + "interface-2": { + "interface": { + "id": 2 + } + }, + "imp": { + "interface": { + "id": 3 + } + } + }, + "exports": {}, + "package": 1 + } + ], + "interfaces": [ + { + "name": "i", + "types": { + "a": 0 + }, + "functions": {}, + "package": 0 + }, + { + "name": null, + "types": { + "a": 1 + }, + "functions": {}, + "package": 0 + }, + { + "name": "i", + "types": { + "a": 2 + }, + "functions": {}, + "package": 1 + }, + { + "name": null, + "types": { + "a": 3 + }, + "functions": {}, + "package": 1 + } + ], + "types": [ + { + "name": "a", + "kind": { + "type": "u32" + }, + "owner": { + "interface": 0 + } + }, + { + "name": "a", + "kind": { + "type": 0 + }, + "owner": { + "interface": 1 + } + }, + { + "name": "a", + "kind": { + "type": "u32" + }, + "owner": { + "interface": 2 + } + }, + { + "name": "a", + "kind": { + "type": 2 + }, + "owner": { + "interface": 3 + } + } + ], + "packages": [ + { + "name": "foo:name", + "interfaces": { + "i": 0 + }, + "worlds": { + "w": 0 + } + }, + { + "name": "bar:name", + "interfaces": { + "i": 2 + }, + "worlds": { + "w": 1 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/packages-explicit-internal-references.wit b/crates/wit-parser/tests/ui/packages-explicit-internal-references.wit new file mode 100644 index 0000000000..52d1c68241 --- /dev/null +++ b/crates/wit-parser/tests/ui/packages-explicit-internal-references.wit @@ -0,0 +1,15 @@ +package foo:name { + interface i1 { + type a = u32; + } +} + +package bar:name { + world w1 { + import imp1: interface { + use foo:name/i1.{a}; + + fn: func(a: a); + } + } +} diff --git a/crates/wit-parser/tests/ui/packages-explicit-internal-references.wit.json b/crates/wit-parser/tests/ui/packages-explicit-internal-references.wit.json new file mode 100644 index 0000000000..1d59a07e3a --- /dev/null +++ b/crates/wit-parser/tests/ui/packages-explicit-internal-references.wit.json @@ -0,0 +1,87 @@ +{ + "worlds": [ + { + "name": "w1", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + }, + "imp1": { + "interface": { + "id": 1 + } + } + }, + "exports": {}, + "package": 1 + } + ], + "interfaces": [ + { + "name": "i1", + "types": { + "a": 0 + }, + "functions": {}, + "package": 0 + }, + { + "name": null, + "types": { + "a": 1 + }, + "functions": { + "fn": { + "name": "fn", + "kind": "freestanding", + "params": [ + { + "name": "a", + "type": 1 + } + ], + "results": [] + } + }, + "package": 1 + } + ], + "types": [ + { + "name": "a", + "kind": { + "type": "u32" + }, + "owner": { + "interface": 0 + } + }, + { + "name": "a", + "kind": { + "type": 0 + }, + "owner": { + "interface": 1 + } + } + ], + "packages": [ + { + "name": "foo:name", + "interfaces": { + "i1": 0 + }, + "worlds": {} + }, + { + "name": "bar:name", + "interfaces": {}, + "worlds": { + "w1": 0 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/packages-explicit-with-semver.wit b/crates/wit-parser/tests/ui/packages-explicit-with-semver.wit new file mode 100644 index 0000000000..583b55595f --- /dev/null +++ b/crates/wit-parser/tests/ui/packages-explicit-with-semver.wit @@ -0,0 +1,23 @@ +package foo:name@1.0.0 { + interface i1 { + type a = u32; + } + + world w1 { + import imp1: interface { + use i1.{a}; + } + } +} + +package foo:name@1.0.1 { + interface i1 { + type a = u32; + } + + world w1 { + import imp1: interface { + use i1.{a}; + } + } +} diff --git a/crates/wit-parser/tests/ui/packages-explicit-with-semver.wit.json b/crates/wit-parser/tests/ui/packages-explicit-with-semver.wit.json new file mode 100644 index 0000000000..23a10462aa --- /dev/null +++ b/crates/wit-parser/tests/ui/packages-explicit-with-semver.wit.json @@ -0,0 +1,130 @@ +{ + "worlds": [ + { + "name": "w1", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + }, + "imp1": { + "interface": { + "id": 1 + } + } + }, + "exports": {}, + "package": 0 + }, + { + "name": "w1", + "imports": { + "interface-2": { + "interface": { + "id": 2 + } + }, + "imp1": { + "interface": { + "id": 3 + } + } + }, + "exports": {}, + "package": 1 + } + ], + "interfaces": [ + { + "name": "i1", + "types": { + "a": 0 + }, + "functions": {}, + "package": 0 + }, + { + "name": null, + "types": { + "a": 1 + }, + "functions": {}, + "package": 0 + }, + { + "name": "i1", + "types": { + "a": 2 + }, + "functions": {}, + "package": 1 + }, + { + "name": null, + "types": { + "a": 3 + }, + "functions": {}, + "package": 1 + } + ], + "types": [ + { + "name": "a", + "kind": { + "type": "u32" + }, + "owner": { + "interface": 0 + } + }, + { + "name": "a", + "kind": { + "type": 0 + }, + "owner": { + "interface": 1 + } + }, + { + "name": "a", + "kind": { + "type": "u32" + }, + "owner": { + "interface": 2 + } + }, + { + "name": "a", + "kind": { + "type": 2 + }, + "owner": { + "interface": 3 + } + } + ], + "packages": [ + { + "name": "foo:name@1.0.0", + "interfaces": { + "i1": 0 + }, + "worlds": { + "w1": 0 + } + }, + { + "name": "foo:name@1.0.1", + "interfaces": { + "i1": 2 + }, + "worlds": { + "w1": 1 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/packages-multiple-explicit.wit b/crates/wit-parser/tests/ui/packages-multiple-explicit.wit new file mode 100644 index 0000000000..79e983765e --- /dev/null +++ b/crates/wit-parser/tests/ui/packages-multiple-explicit.wit @@ -0,0 +1,23 @@ +package foo:name { + interface i1 { + type a = u32; + } + + world w1 { + import imp1: interface { + use i1.{a}; + } + } +} + +package bar:name { + interface i2 { + type b = u32; + } + + world w2 { + import imp2: interface { + use i2.{b}; + } + } +} diff --git a/crates/wit-parser/tests/ui/packages-multiple-explicit.wit.json b/crates/wit-parser/tests/ui/packages-multiple-explicit.wit.json new file mode 100644 index 0000000000..e620764f8e --- /dev/null +++ b/crates/wit-parser/tests/ui/packages-multiple-explicit.wit.json @@ -0,0 +1,130 @@ +{ + "worlds": [ + { + "name": "w1", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + }, + "imp1": { + "interface": { + "id": 1 + } + } + }, + "exports": {}, + "package": 0 + }, + { + "name": "w2", + "imports": { + "interface-2": { + "interface": { + "id": 2 + } + }, + "imp2": { + "interface": { + "id": 3 + } + } + }, + "exports": {}, + "package": 1 + } + ], + "interfaces": [ + { + "name": "i1", + "types": { + "a": 0 + }, + "functions": {}, + "package": 0 + }, + { + "name": null, + "types": { + "a": 1 + }, + "functions": {}, + "package": 0 + }, + { + "name": "i2", + "types": { + "b": 2 + }, + "functions": {}, + "package": 1 + }, + { + "name": null, + "types": { + "b": 3 + }, + "functions": {}, + "package": 1 + } + ], + "types": [ + { + "name": "a", + "kind": { + "type": "u32" + }, + "owner": { + "interface": 0 + } + }, + { + "name": "a", + "kind": { + "type": 0 + }, + "owner": { + "interface": 1 + } + }, + { + "name": "b", + "kind": { + "type": "u32" + }, + "owner": { + "interface": 2 + } + }, + { + "name": "b", + "kind": { + "type": 2 + }, + "owner": { + "interface": 3 + } + } + ], + "packages": [ + { + "name": "foo:name", + "interfaces": { + "i1": 0 + }, + "worlds": { + "w1": 0 + } + }, + { + "name": "bar:name", + "interfaces": { + "i2": 2 + }, + "worlds": { + "w2": 1 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/packages-single-explicit.wit b/crates/wit-parser/tests/ui/packages-single-explicit.wit new file mode 100644 index 0000000000..695155a9af --- /dev/null +++ b/crates/wit-parser/tests/ui/packages-single-explicit.wit @@ -0,0 +1,11 @@ +package foo:name { + interface i1 { + type a = u32; + } + + world w1 { + import imp1: interface { + use i1.{a}; + } + } +} diff --git a/crates/wit-parser/tests/ui/packages-single-explicit.wit.json b/crates/wit-parser/tests/ui/packages-single-explicit.wit.json new file mode 100644 index 0000000000..3eefde2810 --- /dev/null +++ b/crates/wit-parser/tests/ui/packages-single-explicit.wit.json @@ -0,0 +1,70 @@ +{ + "worlds": [ + { + "name": "w1", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + }, + "imp1": { + "interface": { + "id": 1 + } + } + }, + "exports": {}, + "package": 0 + } + ], + "interfaces": [ + { + "name": "i1", + "types": { + "a": 0 + }, + "functions": {}, + "package": 0 + }, + { + "name": null, + "types": { + "a": 1 + }, + "functions": {}, + "package": 0 + } + ], + "types": [ + { + "name": "a", + "kind": { + "type": "u32" + }, + "owner": { + "interface": 0 + } + }, + { + "name": "a", + "kind": { + "type": 0 + }, + "owner": { + "interface": 1 + } + } + ], + "packages": [ + { + "name": "foo:name", + "interfaces": { + "i1": 0 + }, + "worlds": { + "w1": 0 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/explicit-packages-colliding-names.wit b/crates/wit-parser/tests/ui/parse-fail/explicit-packages-colliding-names.wit new file mode 100644 index 0000000000..1e9e0a5c1a --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/explicit-packages-colliding-names.wit @@ -0,0 +1,3 @@ +package foo:name {} + +package foo:name {} diff --git a/crates/wit-parser/tests/ui/parse-fail/explicit-packages-colliding-names.wit.result b/crates/wit-parser/tests/ui/parse-fail/explicit-packages-colliding-names.wit.result new file mode 100644 index 0000000000..fcc55c6e74 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/explicit-packages-colliding-names.wit.result @@ -0,0 +1 @@ +colliding explicit package names, multiple packages named `foo:name` \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/explicit-packages-with-error.wit b/crates/wit-parser/tests/ui/parse-fail/explicit-packages-with-error.wit new file mode 100644 index 0000000000..4fcb27cbc1 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/explicit-packages-with-error.wit @@ -0,0 +1,13 @@ +package foo:name {} + +package bar:name { + interface unused { + type a = u32; + } + + world w { + import imp: interface { + use missing.{a}; + } + } +} diff --git a/crates/wit-parser/tests/ui/parse-fail/explicit-packages-with-error.wit.result b/crates/wit-parser/tests/ui/parse-fail/explicit-packages-with-error.wit.result new file mode 100644 index 0000000000..2c0161f929 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/explicit-packages-with-error.wit.result @@ -0,0 +1,8 @@ +failed to start resolving path: tests/ui/parse-fail/explicit-packages-with-error.wit + +Caused by: + interface or world `missing` does not exist + --> tests/ui/parse-fail/explicit-packages-with-error.wit:10:13 + | + 10 | use missing.{a}; + | ^------ \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/mix-explicit-then-implicit-package.wit b/crates/wit-parser/tests/ui/parse-fail/mix-explicit-then-implicit-package.wit new file mode 100644 index 0000000000..9c208438cd --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/mix-explicit-then-implicit-package.wit @@ -0,0 +1,23 @@ +package foo:name { + interface i1 { + type a = u32; + } + + world w1 { + import imp1: interface { + use i1.{a}; + } + } +} + + +interface i2 { + type b = u32; +} + +world w2 { + import imp2: interface { + use i2.{b}; + } +} + diff --git a/crates/wit-parser/tests/ui/parse-fail/mix-explicit-then-implicit-package.wit.result b/crates/wit-parser/tests/ui/parse-fail/mix-explicit-then-implicit-package.wit.result new file mode 100644 index 0000000000..8a6fa246d3 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/mix-explicit-then-implicit-package.wit.result @@ -0,0 +1 @@ +WIT files cannot mix top-level explicit `package` declarations with other declaration kinds \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/mix-implicit-then-explicit-package.wit b/crates/wit-parser/tests/ui/parse-fail/mix-implicit-then-explicit-package.wit new file mode 100644 index 0000000000..58b934af81 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/mix-implicit-then-explicit-package.wit @@ -0,0 +1,23 @@ +interface i1 { + type a = u32; +} + +world w1 { + import imp1: interface { + use i1.{a}; + } +} + + +package foo:name { + interface i2 { + type b = u32; + } + + world w2 { + import imp2: interface { + use i2.{b}; + } + } +} + diff --git a/crates/wit-parser/tests/ui/parse-fail/mix-implicit-then-explicit-package.wit.result b/crates/wit-parser/tests/ui/parse-fail/mix-implicit-then-explicit-package.wit.result new file mode 100644 index 0000000000..6f71c3a352 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/mix-implicit-then-explicit-package.wit.result @@ -0,0 +1,5 @@ +expected `world`, `interface` or `use`, found keyword `package` + --> tests/ui/parse-fail/mix-implicit-then-explicit-package.wit:12:1 + | + 12 | package foo:name { + | ^------ \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/multiple-packages-no-scope-blocks.wit b/crates/wit-parser/tests/ui/parse-fail/multiple-packages-no-scope-blocks.wit new file mode 100644 index 0000000000..4ebb169224 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/multiple-packages-no-scope-blocks.wit @@ -0,0 +1,15 @@ +package foo:name; + +interface i1 { + type a = u32; +} + +package bar:name; + +world w1 { + import imp1: interface { + use foo:name/i1.{a}; + + fn: func(a: a); + } +} diff --git a/crates/wit-parser/tests/ui/parse-fail/multiple-packages-no-scope-blocks.wit.result b/crates/wit-parser/tests/ui/parse-fail/multiple-packages-no-scope-blocks.wit.result new file mode 100644 index 0000000000..2135ac63be --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/multiple-packages-no-scope-blocks.wit.result @@ -0,0 +1,5 @@ +expected `world`, `interface` or `use`, found keyword `package` + --> tests/ui/parse-fail/multiple-packages-no-scope-blocks.wit:7:1 + | + 7 | package bar:name; + | ^------ \ No newline at end of file diff --git a/crates/wit-smith/src/lib.rs b/crates/wit-smith/src/lib.rs index 36410ada5d..bac7e66a66 100644 --- a/crates/wit-smith/src/lib.rs +++ b/crates/wit-smith/src/lib.rs @@ -22,19 +22,26 @@ pub fn smith(config: &Config, u: &mut Unstructured<'_>) -> Result> { let mut last = None; for pkg in pkgs { let unresolved = pkg.sources.parse().unwrap(); - let id = match resolve.push(unresolved) { - Ok(id) => id, - Err(e) => { - if e.to_string().contains( - "interface transitively depends on an interface in \ - incompatible ways", - ) { - return Err(arbitrary::Error::IncorrectFormat); - } - panic!("bad wit parse: {e:?}") + let first_unresolved = unresolved.into_iter().nth(0); + match first_unresolved { + Some(first_unresolved) => { + let id = match resolve.push(first_unresolved) { + Ok(id) => id, + Err(e) => { + if e.to_string().contains( + "interface transitively depends on an interface in \ + incompatible ways", + ) { + return Err(arbitrary::Error::IncorrectFormat); + } + panic!("bad wit parse: {e:?}") + } + }; + last = Some(id); } - }; - last = Some(id); + // An empty vector means we had a bunch of empty wit files here. + None => return Err(arbitrary::Error::NotEnoughData), + } } let pkg = last.unwrap(); diff --git a/fuzz/src/roundtrip_wit.rs b/fuzz/src/roundtrip_wit.rs index d5e91c584f..24a7ebaadc 100644 --- a/fuzz/src/roundtrip_wit.rs +++ b/fuzz/src/roundtrip_wit.rs @@ -11,17 +11,23 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> { })?; write_file("doc1.wasm", &wasm); let (resolve, _pkg) = match wit_component::decode(&wasm).unwrap() { - DecodedWasm::WitPackage(resolve, pkg) => (resolve, pkg), + DecodedWasm::WitPackages(resolve, pkg) => (resolve, pkg), DecodedWasm::Component(..) => unreachable!(), }; roundtrip_through_printing("doc1", &resolve, &wasm); - let (resolve2, pkg2) = match wit_component::decode(&wasm).unwrap() { - DecodedWasm::WitPackage(resolve, pkg) => (resolve, pkg), + let (resolve2, pkgs2) = match wit_component::decode(&wasm).unwrap() { + DecodedWasm::WitPackages(resolve, pkgs) => (resolve, pkgs), DecodedWasm::Component(..) => unreachable!(), }; + // wit_smith returns WIT source with only a single package. + if pkgs2.len() != 1 { + panic!("rountrip WIT test smithed file with multiple packages") + } + + let pkg2 = pkgs2[0]; let wasm2 = wit_component::encode(Some(true), &resolve2, pkg2).expect("failed to encode WIT document"); write_file("doc2.wasm", &wasm2); @@ -80,12 +86,12 @@ fn roundtrip_through_printing(file: &str, resolve: &Resolve, wasm: &[u8]) { for (id, pkg) in resolve.packages.iter() { let mut map = SourceMap::new(); let pkg_name = &pkg.name; - let doc = WitPrinter::default().print(resolve, id).unwrap(); + let doc = WitPrinter::default().print(resolve, vec![id]).unwrap(); 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.push(unresolved).unwrap(); - last = Some(id); + let id = new_resolve.append(unresolved).unwrap(); + last = Some(id.last().unwrap().to_owned()); } // Finally encode the `new_resolve` which should be the exact same as diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs index 366d325fda..6e68c94524 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -11,7 +11,8 @@ use wasm_tools::Output; use wasmparser::WasmFeatures; use wat::Detect; use wit_component::{ - embed_component_metadata, ComponentEncoder, DecodedWasm, Linker, StringEncoding, WitPrinter, + embed_component_metadata, resolve_world_from_name, ComponentEncoder, DecodedWasm, + Linker, StringEncoding, WitPrinter, }; use wit_parser::{PackageId, Resolve, UnresolvedPackage}; @@ -199,10 +200,10 @@ impl WitResolve { return resolve; } - fn load(&self) -> Result<(Resolve, PackageId)> { + fn load(&self) -> Result<(Resolve, Vec)> { let mut resolve = Self::resolve_with_features(&self.features); - let id = resolve.push_path(&self.wit)?.0; - Ok((resolve, id)) + let pkg_ids = resolve.push_path(&self.wit)?.iter().map(|p| p.0).collect(); + Ok((resolve, pkg_ids)) } } @@ -239,11 +240,12 @@ pub struct EmbedOpts { /// The world that the component uses. /// - /// This is the path, within the `WIT` package provided as a positional - /// argument, to the `world` that the core wasm module works with. This can - /// either be a bare string which a document name that has a `default - /// world`, or it can be a `foo/bar` name where `foo` names a document and - /// `bar` names a world within that document. + /// This is the path, within the `WIT` source provided as a positional argument, to the `world` + /// that the core wasm module works with. This can either be a bare string which is a document + /// name that has a `default world`, or it can be a `foo/bar` name where `foo` names a document + /// and `bar` names a world within that document. If the `WIT` source provided contains multiple + /// packages, this option must be set, and must be of the fully-qualified form (ex: + /// "wasi:http/proxy") #[clap(short, long)] world: Option, @@ -275,9 +277,8 @@ impl EmbedOpts { } else { Some(self.io.parse_input_wasm()?) }; - let (resolve, id) = self.resolve.load()?; - let world = resolve.select_world(id, self.world.as_deref())?; - + let (resolve, pkg_ids) = self.resolve.load()?; + let world = resolve_world_from_name(&resolve, pkg_ids, self.world.as_deref())?; let mut wasm = wasm.unwrap_or_else(|| wit_component::dummy_module(&resolve, world)); embed_component_metadata( @@ -531,8 +532,8 @@ impl WitOpts { if let Some(input) = &self.input { if input.is_dir() { let mut resolve = WitResolve::resolve_with_features(&self.features); - let id = resolve.push_dir(&input)?.0; - return Ok(DecodedWasm::WitPackage(resolve, id)); + let ids = resolve.push_dir(&input)?.iter().map(|p| p.0).collect(); + return Ok(DecodedWasm::WitPackages(resolve, ids)); } } @@ -579,9 +580,9 @@ impl WitOpts { Err(_) => bail!("input was not valid utf-8"), }; let mut resolve = WitResolve::resolve_with_features(&self.features); - let pkg = UnresolvedPackage::parse(path, input)?; - let id = resolve.push(pkg)?; - Ok(DecodedWasm::WitPackage(resolve, id)) + let pkgs = UnresolvedPackage::parse(path, input)?; + let ids = resolve.append(pkgs)?; + Ok(DecodedWasm::WitPackages(resolve, ids)) } } } @@ -589,8 +590,12 @@ impl WitOpts { fn emit_wasm(&self, decoded: &DecodedWasm) -> Result<()> { assert!(self.wasm || self.wat); assert!(self.out_dir.is_none()); + if decoded.packages().len() != 1 { + bail!("emitting WASM for multi-package WIT files is not yet supported") + } - let bytes = wit_component::encode(None, decoded.resolve(), decoded.package())?; + let decoded_package = decoded.packages()[0]; + let bytes = wit_component::encode(None, decoded.resolve(), decoded_package)?; if !self.skip_validation { wasmparser::Validator::new_with_features( WasmFeatures::default() | WasmFeatures::COMPONENT_MODEL, @@ -608,7 +613,7 @@ impl WitOpts { assert!(!self.wasm && !self.wat); let resolve = decoded.resolve(); - let main = decoded.package(); + let main = decoded.packages(); let mut printer = WitPrinter::default(); printer.emit_docs(!self.no_docs); @@ -632,8 +637,8 @@ impl WitOpts { } for (id, pkg) in resolve.packages.iter() { - let output = printer.print(resolve, id)?; - let out_dir = if id == main { + let output = printer.print(resolve, vec![id])?; + let out_dir = if main.contains(&id) { dir.clone() } else { let dir = dir.join("deps"); @@ -687,7 +692,9 @@ pub struct TargetsOpts { #[clap(flatten)] resolve: WitResolve, - /// The world used to test whether a component conforms to its signature. + /// The world used to test whether a component conforms to its signature. If the `WIT` source + /// provided contains multiple packages, this option must be set, and must be of the + /// fully-qualified form (ex: "wasi:http/proxy") #[clap(short, long)] world: Option, @@ -702,8 +709,8 @@ impl TargetsOpts { /// Executes the application. fn run(self) -> Result<()> { - let (resolve, package_id) = self.resolve.load()?; - let world = resolve.select_world(package_id, self.world.as_deref())?; + let (resolve, pkg_ids) = self.resolve.load()?; + let world = resolve_world_from_name(&resolve, pkg_ids, self.world.as_deref())?; let component_to_test = self.input.parse_wasm()?; wit_component::targets(&resolve, world, &component_to_test)?; @@ -742,9 +749,9 @@ impl SemverCheckOpts { } fn run(self) -> Result<()> { - let (resolve, package_id) = self.resolve.load()?; - let prev = resolve.select_world(package_id, Some(&self.prev))?; - let new = resolve.select_world(package_id, Some(&self.new))?; + 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())?; wit_component::semver_check(resolve, prev, new)?; Ok(()) } diff --git a/tests/cli/print-core-wasm-wit-multiple-packages.wit b/tests/cli/print-core-wasm-wit-multiple-packages.wit new file mode 100644 index 0000000000..66a2ac2dd0 --- /dev/null +++ b/tests/cli/print-core-wasm-wit-multiple-packages.wit @@ -0,0 +1,21 @@ +// RUN: component embed --dummy --world=bar:bar/my-world % | component wit + +package foo:foo { + interface my-interface { + foo: func(); + } + + world my-world { + import my-interface; + } +} + +package bar:bar { + interface my-interface { + foo: func(); + } + + world my-world { + import my-interface; + } +} diff --git a/tests/cli/print-core-wasm-wit-multiple-packages.wit.stdout b/tests/cli/print-core-wasm-wit-multiple-packages.wit.stdout new file mode 100644 index 0000000000..75feb032d8 --- /dev/null +++ b/tests/cli/print-core-wasm-wit-multiple-packages.wit.stdout @@ -0,0 +1,5 @@ +package root:root; + +world root { + import bar:bar/my-interface; +} diff --git a/tests/cli/semver-check-different-packages.wit b/tests/cli/semver-check-different-packages.wit new file mode 100644 index 0000000000..2a666e5e11 --- /dev/null +++ b/tests/cli/semver-check-different-packages.wit @@ -0,0 +1,12 @@ +// FAIL: component semver-check % --prev a:b/prev --new c:d/new + +package a:b { + world prev {} +} + +package c:d { + world new { + import a: func(); + import b: interface {} + } +} diff --git a/tests/cli/semver-check-different-packages.wit.stderr b/tests/cli/semver-check-different-packages.wit.stderr new file mode 100644 index 0000000000..c9dfe93a02 --- /dev/null +++ b/tests/cli/semver-check-different-packages.wit.stderr @@ -0,0 +1 @@ +error: the old world is in package a:b, which is not the same as the new world, which is in package c:d diff --git a/tests/cli/semver-check-multiple-packages.wit b/tests/cli/semver-check-multiple-packages.wit new file mode 100644 index 0000000000..49f23c39ce --- /dev/null +++ b/tests/cli/semver-check-multiple-packages.wit @@ -0,0 +1,23 @@ +// RUN: component semver-check % --prev a:b/prev --new a:b/next + +package a:b { + world prev {} + + interface next-interface { + + } + + world next { + import a: func(); + import b: interface {} + import next-interface; + } +} + +package c:d { + world old {} + + world new { + import a: func(); + } +}