diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index 336266dead..057e755c51 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -2133,33 +2133,33 @@ mod test { use super::*; use std::path::Path; - use wit_parser::UnresolvedPackage; + use wit_parser::UnresolvedPackageGroup; #[test] fn it_renames_imports() { let mut resolve = Resolve::new(); - let pkg = resolve - .push( - UnresolvedPackage::parse( - Path::new("test.wit"), - r#" + let UnresolvedPackageGroup { + mut packages, + source_map, + } = UnresolvedPackageGroup::parse( + Path::new("test.wit"), + r#" package test:wit; interface i { - f: func(); +f: func(); } world test { - import i; - import foo: interface { - f: func(); - } +import i; +import foo: interface { +f: func(); +} } "#, - ) - .unwrap(), - ) - .unwrap(); + ) + .unwrap(); + let pkg = resolve.push(packages.remove(0), &source_map).unwrap(); let world = resolve.select_world(pkg, None).unwrap(); diff --git a/crates/wit-component/src/lib.rs b/crates/wit-component/src/lib.rs index 9b3be71c59..7d81335e23 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 { @@ -112,7 +149,7 @@ mod tests { use anyhow::Result; use wasmparser::Payload; - use wit_parser::{Resolve, UnresolvedPackage}; + use wit_parser::{Resolve, UnresolvedPackageGroup}; use super::{embed_component_metadata, StringEncoding}; @@ -147,8 +184,11 @@ 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_id = resolver.push(pkg)?; + let UnresolvedPackageGroup { + mut packages, + source_map, + } = UnresolvedPackageGroup::parse(&Path::new("in-code.wit"), COMPONENT_WIT)?; + let pkg_id = resolver.push(packages.remove(0), &source_map)?; let world = resolver.select_world(pkg_id, Some("test-world").into())?; // Embed component metadata 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..28406a5023 100644 --- a/crates/wit-component/src/printing.rs +++ b/crates/wit-component/src/printing.rs @@ -50,37 +50,56 @@ 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: &[PackageId]) -> Result { + let has_multiple_packages = pkg_ids.len() > 1; + 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.clone()]; + 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}")); + } + + if has_multiple_packages { + self.output.push_str("{"); + self.output.indent += 1 + } else { + 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, "}}")?; + } + + if has_multiple_packages { + self.output.push_str("}"); + self.output.indent -= 1 + } } 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..87917cce30 100644 --- a/crates/wit-component/tests/components.rs +++ b/crates/wit-component/tests/components.rs @@ -4,7 +4,7 @@ use pretty_assertions::assert_eq; use std::{borrow::Cow, fs, path::Path}; use wasm_encoder::{Encode, Section}; use wit_component::{ComponentEncoder, DecodedWasm, Linker, StringEncoding, WitPrinter}; -use wit_parser::{PackageId, Resolve, UnresolvedPackage}; +use wit_parser::{PackageId, Resolve, UnresolvedPackageGroup}; /// Tests the encoding of components. /// @@ -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 (pkg_ids, _) = resolve.push_dir(&path)?; + let pkg_count = pkg_ids.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 pkg_ids { + // 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, &[pkg]) + .context("failed to print WIT")?; + assert_output(&wit, &component_wit_path)?; - UnresolvedPackage::parse(&component_wit_path, &wit).context("failed to parse printed WIT")?; + UnresolvedPackageGroup::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..926499db9c 100644 --- a/crates/wit-component/tests/interfaces.rs +++ b/crates/wit-component/tests/interfaces.rs @@ -5,7 +5,7 @@ use std::fs; use std::path::Path; use wasmparser::WasmFeatures; use wit_component::WitPrinter; -use wit_parser::{PackageId, Resolve, UnresolvedPackage}; +use wit_parser::{PackageId, Resolve, UnresolvedPackageGroup}; /// Tests the encoding of a WIT package as a WebAssembly binary. /// @@ -48,55 +48,66 @@ fn main() -> Result<()> { fn run_test(path: &Path, is_dir: bool) -> Result<()> { let mut resolve = Resolve::new(); - let package = if is_dir { + let packages = if is_dir { resolve.push_dir(path)?.0 } else { - resolve.push(UnresolvedPackage::parse_file(path)?)? + resolve.append(UnresolvedPackageGroup::parse_file(path)?)? }; - assert_print(&resolve, package, path, is_dir)?; + for package in packages { + assert_print(&resolve, &[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: &[PackageId], path: &Path, is_dir: bool) -> Result<()> { + let output = WitPrinter::default().print(resolve, &pkg_ids)?; + 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) + UnresolvedPackageGroup::parse("foo.wit".as_ref(), &output) .context("failed to parse printed output")?; Ok(()) } diff --git a/crates/wit-component/tests/linking.rs b/crates/wit-component/tests/linking.rs index b774827dc1..56757f9750 100644 --- a/crates/wit-component/tests/linking.rs +++ b/crates/wit-component/tests/linking.rs @@ -2,7 +2,7 @@ use { anyhow::{Context, Result}, std::path::Path, wit_component::StringEncoding, - wit_parser::{Resolve, UnresolvedPackage}, + wit_parser::{Resolve, UnresolvedPackageGroup}, }; const FOO: &str = r#" @@ -141,7 +141,11 @@ 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 UnresolvedPackageGroup { + mut packages, + source_map, + } = UnresolvedPackageGroup::parse(Path::new("wit"), wit)?; + let pkg = resolve.push(packages.remove(0), &source_map)?; let world = resolve.select_world(pkg, None)?; wit_component::embed_component_metadata( diff --git a/crates/wit-component/tests/merge.rs b/crates/wit-component/tests/merge.rs index 264c0c0d59..f9a70859bb 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, &[id])?; assert_output(&expected, &output)?; } } diff --git a/crates/wit-component/tests/targets.rs b/crates/wit-component/tests/targets.rs index 23579b7f50..8dac12d304 100644 --- a/crates/wit-component/tests/targets.rs +++ b/crates/wit-component/tests/targets.rs @@ -1,6 +1,6 @@ -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use std::{fs, path::Path}; -use wit_parser::{Resolve, UnresolvedPackage, WorldId}; +use wit_parser::{Resolve, UnresolvedPackageGroup, WorldId}; /// Tests whether a component targets a world. /// @@ -79,11 +79,20 @@ 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 = - UnresolvedPackage::parse_file(&test_wit_path).context("failed to parse WIT package")?; + let UnresolvedPackageGroup { + mut packages, + source_map, + } = UnresolvedPackageGroup::parse_file(&test_wit_path) + .context("failed to parse WIT package")?; + if packages.is_empty() { + bail!("Files were completely empty - are you sure these are the files you're looking for?") + } + if packages.len() > 1 { + bail!("Multi-package targeting tests are not yet supported.") + } let mut resolve = Resolve::default(); - let package_id = resolve.push(package)?; + let package_id = resolve.push(packages.remove(0), &source_map)?; let world_id = resolve .select_world(package_id, Some(TEST_TARGET_WORLD_ID)) diff --git a/crates/wit-component/tests/wit.rs b/crates/wit-component/tests/wit.rs index 92d7713288..860fd1082c 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()); @@ -25,7 +25,7 @@ fn parse_wit_file() -> Result<()> { let mut resolver = Resolve::default(); let package_id = resolver .push_path("tests/wit/parse-dir/wit/deps/bar/bar.wit")? - .0; + .0[0]; resolver.select_world(package_id, "bar-world".into())?; assert!(resolver .interfaces diff --git a/crates/wit-parser/fuzz/fuzz_targets/parse.rs b/crates/wit-parser/fuzz/fuzz_targets/parse.rs index 4bc47fb839..b9d21dc4f5 100644 --- a/crates/wit-parser/fuzz/fuzz_targets/parse.rs +++ b/crates/wit-parser/fuzz/fuzz_targets/parse.rs @@ -10,5 +10,8 @@ fuzz_target!(|data: &[u8]| { Err(_) => return, }; - drop(wit_parser::UnresolvedPackage::parse("foo".as_ref(), &data)); + drop(wit_parser::UnresolvedPackageGroup::parse( + "foo".as_ref(), + &data, + )); }); diff --git a/crates/wit-parser/src/ast.rs b/crates/wit-parser/src/ast.rs index 22a7d7d179..d65d5fed4f 100644 --- a/crates/wit-parser/src/ast.rs +++ b/crates/wit-parser/src/ast.rs @@ -1,8 +1,9 @@ -use crate::{Error, UnresolvedPackage}; +use crate::{Error, UnresolvedPackage, UnresolvedPackageGroup}; 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,131 @@ 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>, } -impl<'a> Ast<'a> { - pub fn parse(lexer: &mut Tokenizer<'a>) -> Result { +impl<'a> DeclList<'a> { + fn parse_explicit_package_items(tokens: &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)?; + 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)?; } - while lexer.clone().next()?.is_some() { - items.push(AstItem::parse(lexer, docs)?); - docs = parse_docs(lexer)?; + } + + fn parse_implicit_package_items( + 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(Self { package_id, items }) + Ok(DeclList { items }) } +} +impl<'a> Ast<'a> { + 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: DeclList::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; + } + } + + if packages.is_empty() { + return Ok(Ast::PartialImplicitPackage(PartialImplicitPackage { + package_id: maybe_package_id, + decl_list: DeclList::parse_implicit_package_items(tokens, docs)?, + })); + } + Ok(Ast::ExplicitPackages(packages)) + } +} + +impl<'a> DeclList<'a> { fn for_each_path<'b>( &'b self, mut f: impl FnMut( @@ -1483,6 +1586,40 @@ struct Source { contents: String, } +enum ResolverKind<'a> { + Unknown, + Explicit(Vec), + PartialImplicit(Resolver<'a>), +} + +fn parse_package( + unparsed_pkgs: Vec, + src: &Source, + explicit_pkg_names: &mut HashSet, + parsed_pkgs: &mut Vec, +) -> Result<()> { + for pkg in unparsed_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 { + parsed_pkgs.push(unresolved); + } + } + Ok(()) +} + impl SourceMap { /// Creates a new empty source map. pub fn new() -> SourceMap { @@ -1532,10 +1669,11 @@ 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 resolver_kind = ResolverKind::Unknown; + let parsed_pkgs = self.rewrite_error(|| { + let mut explicit_pkg_names: HashSet = HashSet::new(); let mut srcs = self.sources.iter().collect::>(); srcs.sort_by_key(|src| &src.path); for src in srcs { @@ -1548,15 +1686,55 @@ 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) => { + match &mut resolver_kind { + ResolverKind::Unknown => { + let mut parsed_pkgs = Vec::new(); + parse_package(pkgs, src, &mut explicit_pkg_names, &mut parsed_pkgs)?; + resolver_kind = ResolverKind::Explicit(parsed_pkgs); + }, + ResolverKind::Explicit(parsed_pkgs) => { + parse_package(pkgs, src, &mut explicit_pkg_names, parsed_pkgs)?; + }, + ResolverKind::PartialImplicit(_) => bail!("WIT files cannot mix top-level explicit `package` declarations with other declaration kinds"), + } + } + Ast::PartialImplicitPackage(partial) => { + match &mut resolver_kind { + ResolverKind::Unknown => { + let mut resolver = Resolver::default(); + resolver.push_partial(partial).with_context(|| { + format!("failed to start resolving path: {}", src.path.display()) + })?; + resolver_kind = ResolverKind::PartialImplicit(resolver); + }, + ResolverKind::Explicit(_) => bail!("WIT files cannot mix top-level explicit `package` declarations with other declaration kinds"), + ResolverKind::PartialImplicit(resolver) => { + resolver.push_partial(partial).with_context(|| { + format!("failed to start resolving path: {}", src.path.display()) + })?; + } + } + } + } + } + + match resolver_kind { + ResolverKind::Unknown => bail!("No WIT packages found in the supplied source"), + ResolverKind::Explicit(pkgs) => Ok(pkgs), + ResolverKind::PartialImplicit(mut resolver) => match resolver.resolve()? { + Some(pkg) => Ok(vec![pkg]), + None => bail!("No WIT packages found in the supplied source"), + }, } - resolver.resolve() })?; - doc.source_map = self; - Ok(doc) + + Ok(UnresolvedPackageGroup { + packages: parsed_pkgs, + source_map: self, + }) } pub(crate) fn rewrite_error(&self, f: F) -> Result @@ -1653,21 +1831,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 51a4623cfa..93ad600872 100644 --- a/crates/wit-parser/src/ast/lex.rs +++ b/crates/wit-parser/src/ast/lex.rs @@ -190,6 +190,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..0d3465a602 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), @@ -222,61 +227,71 @@ impl<'a> Resolver<'a> { world_spans: mem::take(&mut self.world_spans), type_spans: mem::take(&mut self.type_spans), 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 +338,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 +358,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 +372,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 +383,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 +395,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 +414,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 +424,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 +436,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 +488,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 +548,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..1c61ecfd48 100644 --- a/crates/wit-parser/src/decoding.rs +++ b/crates/wit-parser/src/decoding.rs @@ -2,6 +2,7 @@ use crate::*; use anyhow::{anyhow, bail}; use indexmap::IndexSet; use std::mem; +use std::slice; use std::{collections::HashMap, io::Read}; use wasmparser::Chunk; use wasmparser::{ @@ -344,12 +345,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 +361,18 @@ 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) -> &[PackageId] { match self { - DecodedWasm::WitPackage(_, id) => *id, - DecodedWasm::Component(resolve, world) => resolve.worlds[*world].package.unwrap(), + DecodedWasm::WitPackages(_, ids) => ids, + DecodedWasm::Component(resolve, world) => { + slice::from_ref(&resolve.worlds[*world].package.as_ref().unwrap()) + } } } } @@ -383,12 +386,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..e227f7fc0a 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; @@ -110,10 +111,19 @@ pub struct UnresolvedPackage { world_spans: Vec, type_spans: Vec, foreign_dep_spans: Vec, - source_map: SourceMap, required_resource_types: Vec<(TypeId, Span)>, } +/// Tracks a set of packages, all pulled from the same group of WIT source files. +#[derive(Default)] +pub struct UnresolvedPackageGroup { + /// A set of packages that share source file(s). + pub packages: Vec, + + /// A set of processed source files from which these packages have been parsed. + pub source_map: SourceMap, +} + #[derive(Clone)] struct WorldSpan { span: Span, @@ -209,12 +219,17 @@ impl fmt::Display for Error { impl std::error::Error for Error {} -impl UnresolvedPackage { +impl UnresolvedPackageGroup { + /// Creates an empty set of packages. + pub fn new() -> UnresolvedPackageGroup { + UnresolvedPackageGroup::default() + } + /// Parses the given string as a wit document. /// /// The `path` argument is used for error reporting. The `contents` provided /// will not be able to use `pkg` use paths to other documents. - pub fn parse(path: &Path, contents: &str) -> Result { + pub fn parse(path: &Path, contents: &str) -> Result { let mut map = SourceMap::default(); map.push(path, contents); map.parse() @@ -223,13 +238,13 @@ impl UnresolvedPackage { /// Parse a WIT package at the provided path. /// /// 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 { + /// is parsed with [`UnresolvedPackageGroup::parse_file`] and a directory is + /// parsed with [`UnresolvedPackageGroup::parse_dir`]. + pub fn parse_path(path: &Path) -> Result { if path.is_dir() { - UnresolvedPackage::parse_dir(path) + UnresolvedPackageGroup::parse_dir(path) } else { - UnresolvedPackage::parse_file(path) + UnresolvedPackageGroup::parse_file(path) } } @@ -237,7 +252,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 +262,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)? { @@ -273,12 +288,6 @@ impl UnresolvedPackage { } map.parse() } - - /// Returns an iterator over the list of source files that were read when - /// parsing this package. - pub fn source_files(&self) -> impl Iterator { - self.source_map.source_files() - } } #[derive(Debug, Clone)] diff --git a/crates/wit-parser/src/resolve.rs b/crates/wit-parser/src/resolve.rs index fa294f986d..8a34a8e667 100644 --- a/crates/wit-parser/src/resolve.rs +++ b/crates/wit-parser/src/resolve.rs @@ -1,11 +1,12 @@ 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::{ AstItem, Docs, Error, Function, FunctionKind, Handle, IncludeName, Interface, InterfaceId, - InterfaceSpan, PackageName, Results, Stability, Type, TypeDef, TypeDefKind, TypeId, TypeOwner, - UnresolvedPackage, World, WorldId, WorldItem, WorldKey, WorldSpan, + InterfaceSpan, PackageName, Results, SourceMap, Stability, Type, TypeDef, TypeDefKind, TypeId, + TypeOwner, UnresolvedPackage, UnresolvedPackageGroup, World, WorldId, WorldItem, WorldKey, + WorldSpan, }; use anyhow::{anyhow, bail, Context, Result}; use id_arena::{Arena, Id}; @@ -105,7 +106,42 @@ pub type PackageId = Id; enum ParsedFile { #[cfg(feature = "decoding")] Package(PackageId), - Unresolved(UnresolvedPackage), + Unresolved(UnresolvedPackageGroup), +} + +/// Visitor helper for performing topological sort on a group of packages. +fn visit<'a>( + pkg: &'a UnresolvedPackage, + pkg_details_map: &'a BTreeMap, + order: &mut IndexSet, + visiting: &mut HashSet<&'a PackageName>, + source_maps: &[SourceMap], +) -> Result<()> { + if order.contains(&pkg.name) { + return Ok(()); + } + + match pkg_details_map.get(&pkg.name) { + Some(pkg_details) => { + let (_, source_maps_index) = pkg_details; + source_maps[*source_maps_index].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) = pkg_details_map.get(dep) { + let (dep_pkg, _) = dep; + visit(dep_pkg, pkg_details_map, order, visiting, source_maps)?; + } + assert!(visiting.remove(dep)); + } + assert!(order.insert(pkg.name.clone())); + Ok(()) + }) + } + None => panic!("No pkg_details found for package when doing topological sort"), + } } impl Resolve { @@ -131,11 +167,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<(Vec, Vec)> { self._push_path(path.as_ref()) } - fn _push_path(&mut self, path: &Path) -> Result<(PackageId, Vec)> { + fn _push_path(&mut self, path: &Path) -> Result<(Vec, Vec)> { if path.is_dir() { self.push_dir(path).with_context(|| { format!( @@ -144,15 +180,72 @@ impl Resolve { ) }) } else { - let id = self.push_file(path)?; - Ok((id, vec![path.to_path_buf()])) + let ids = self.push_file(path)?; + Ok((ids, vec![path.to_path_buf()])) + } + } + + fn sort_unresolved_packages( + &mut self, + unresolved_groups: Vec, + ) -> Result<(Vec, Vec)> { + let mut pkg_ids = Vec::new(); + let mut path_bufs = Vec::new(); + let mut pkg_details_map = BTreeMap::new(); + let mut source_maps = Vec::new(); + for (i, unresolved_group) in unresolved_groups.into_iter().enumerate() { + let UnresolvedPackageGroup { + packages, + source_map, + } = unresolved_group; + + source_maps.push(source_map); + for pkg in packages { + pkg_details_map.insert(pkg.name.clone(), (pkg, i)); + } + } + + // 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_details in pkg_details_map.values() { + let (pkg, _) = pkg_details; + visit( + pkg, + &pkg_details_map, + &mut order, + &mut visiting, + &source_maps, + )?; + } + + // Ensure that the final output is topologically sorted. Use a set to ensure that we render + // the buffers for each `SourceMap` only once, even though multiple packages may references + // the same `SourceMap`. + let mut seen_source_maps = HashSet::new(); + for name in order { + match pkg_details_map.remove(&name) { + Some((pkg, source_map_index)) => { + let source_map = &source_maps[source_map_index]; + if !seen_source_maps.contains(&source_map_index) { + seen_source_maps.insert(source_map_index); + path_bufs.extend(source_map.source_files().map(|p| p.to_path_buf())); + } + pkg_ids.push(self.push(pkg, source_map)?); + } + None => panic!("package in topologically ordered set, but not in package map"), + } } + + Ok((pkg_ids, path_bufs)) } /// Parses the filesystem directory at `path` as a WIT package and returns /// the fully resolved [`PackageId`] as a result. /// - /// The directory itself is parsed with [`UnresolvedPackage::parse_dir`] + /// The directory itself is parsed with [`UnresolvedPackageGroup::parse_dir`] /// which has more information on the layout of the directory. This method, /// however, additionally supports an optional `deps` dir where dependencies /// can be located. @@ -160,79 +253,39 @@ impl Resolve { /// All entries in the `deps` directory are inspected and parsed as follows: /// /// * Any directories inside of `deps` are assumed to be another WIT package - /// and are parsed with [`UnresolvedPackage::parse_dir`]. - /// * WIT files (`*.wit`) are parsed with [`UnresolvedPackage::parse_file`]. + /// and are parsed with [`UnresolvedPackageGroup::parse_dir`]. + /// * WIT files (`*.wit`) are parsed with [`UnresolvedPackageGroup::parse_file`]. /// * WebAssembly files (`*.wasm` or `*.wat`) are assumed to be WIT packages /// encoded to wasm and are parsed and inserted into `self`. /// - /// This function returns the [`PackageId`] of the root parsed package at + /// This function returns the [`PackageId`]s of the root parsed packages at /// `path`, along with a list of all paths that were consumed during parsing - /// for the root package and all dependency packages. - 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<(Vec, Vec)> { + let deps_path = path.join("deps"); + let unresolved_deps = self.parse_deps_dir(&deps_path).with_context(|| { + format!( + "failed to parse dependency directory: {}", + deps_path.display() + ) + })?; + let (_, mut path_bufs) = self.sort_unresolved_packages(unresolved_deps)?; + + let unresolved_top_level = UnresolvedPackageGroup::parse_dir(path) .with_context(|| format!("failed to parse package: {}", path.display()))?; + let (pkgs_ids, mut top_level_path_bufs) = + self.sort_unresolved_packages(vec![unresolved_top_level])?; - 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(()) - }) - } + path_bufs.append(&mut top_level_path_bufs); + Ok((pkgs_ids, path_bufs)) } - fn parse_deps_dir(&mut self, path: &Path) -> Result> { - let mut ret = BTreeMap::new(); + fn parse_deps_dir(&mut self, path: &Path) -> Result> { + let mut unresolved_deps = Vec::new(); // If there's no `deps` dir, then there's no deps, so return the // empty set. if !path.exists() { - return Ok(ret); + return Ok(unresolved_deps); } let mut entries = path .read_dir() @@ -241,38 +294,37 @@ 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() { + 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` // directories. - UnresolvedPackage::parse_dir(&path) - .with_context(|| format!("failed to parse package: {}", path.display()))? + unresolved_deps.push( + UnresolvedPackageGroup::parse_dir(&path) + .with_context(|| format!("failed to parse package: {}", path.display()))?, + ) } else { // If this entry is a file then we may want to ignore it but // this may also be a standalone WIT file or a `*.wasm` or // `*.wat` encoded package. let filename = dep.file_name(); - match Path::new(&filename).extension().and_then(|s| s.to_str()) { - Some("wit") | Some("wat") | Some("wasm") => match self._push_file(&path)? { - #[cfg(feature = "decoding")] - ParsedFile::Package(_) => continue, - ParsedFile::Unresolved(pkg) => pkg, + unresolved_deps.push( + match Path::new(&filename).extension().and_then(|s| s.to_str()) { + Some("wit") | Some("wat") | Some("wasm") => match self._push_file(&path)? { + #[cfg(feature = "decoding")] + ParsedFile::Package(_) => continue, + ParsedFile::Unresolved(pkgs) => pkgs, + }, + + // Other files in deps dir are ignored for now to avoid + // accidentally including things like `.DS_Store` files in + // the call below to `parse_dir`. + _ => continue, }, - - // Other files in deps dir are ignored for now to avoid - // accidentally including things like `.DS_Store` files in - // the call below to `parse_dir`. - _ => continue, - } + ) }; - let prev = ret.insert(pkg.name.clone(), pkg); - if let Some(prev) = prev { - bail!("duplicate definitions of package `{}` found", prev.name); - } } - Ok(ret) + Ok(unresolved_deps) } /// Parses the contents of `path` from the filesystem and pushes the result @@ -288,11 +340,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 +374,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 +387,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 = UnresolvedPackageGroup::parse(path, text)?; + Ok(ParsedFile::Unresolved(pkgs)) } /// Appends a new [`UnresolvedPackage`] to this [`Resolve`], creating a @@ -347,13 +399,32 @@ 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); + pub fn push( + &mut self, + unresolved: UnresolvedPackage, + source_map: &SourceMap, + ) -> Result { source_map.rewrite_error(|| Remap::default().append(self, unresolved)) } + /// Appends new [`UnresolvedPackageSet`] to this [`Resolve`], creating a + /// fully resolved package with no dangling references. + /// + /// The `deps` argument indicates that the named dependencies in + /// `unresolved` to packages are resolved by the mapping specified. + /// + /// Any dependency resolution error or otherwise world-elaboration error + /// will be returned here, if successful a package identifier is returned + /// which corresponds to the package that was just inserted. + /// + /// The returned [PackageId]s are listed in topologically sorted order. + pub fn append(&mut self, unresolved_groups: UnresolvedPackageGroup) -> Result> { + let (pkg_ids, _) = self.sort_unresolved_packages(vec![unresolved_groups])?; + Ok(pkg_ids) + } + pub fn all_bits_valid(&self, ty: &Type) -> bool { match ty { Type::U8 @@ -454,6 +525,7 @@ impl Resolve { packages, package_names, features: _, + .. } = resolve; let mut moved_types = Vec::new(); @@ -760,8 +832,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 +2564,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::UnresolvedPackageGroup::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..a2dea31719 --- /dev/null +++ b/crates/wit-parser/tests/ui/multi-file-multi-package.wit.json @@ -0,0 +1,250 @@ +{ + "worlds": [ + { + "name": "w2", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + }, + "imp2": { + "interface": { + "id": 1 + } + } + }, + "exports": {}, + "package": 0 + }, + { + "name": "w3", + "imports": { + "interface-2": { + "interface": { + "id": 2 + } + }, + "imp3": { + "interface": { + "id": 3 + } + } + }, + "exports": {}, + "package": 1 + }, + { + "name": "w1", + "imports": { + "interface-4": { + "interface": { + "id": 4 + } + }, + "imp1": { + "interface": { + "id": 5 + } + } + }, + "exports": {}, + "package": 2 + }, + { + "name": "w4", + "imports": { + "interface-6": { + "interface": { + "id": 6 + } + }, + "imp4": { + "interface": { + "id": 7 + } + } + }, + "exports": {}, + "package": 3 + } + ], + "interfaces": [ + { + "name": "i2", + "types": { + "b": 0 + }, + "functions": {}, + "package": 0 + }, + { + "name": null, + "types": { + "b": 1 + }, + "functions": {}, + "package": 0 + }, + { + "name": "i3", + "types": { + "a": 2 + }, + "functions": {}, + "package": 1 + }, + { + "name": null, + "types": { + "a": 3 + }, + "functions": {}, + "package": 1 + }, + { + "name": "i1", + "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": "b", + "kind": { + "type": "u32" + }, + "owner": { + "interface": 0 + } + }, + { + "name": "b", + "kind": { + "type": 0 + }, + "owner": { + "interface": 1 + } + }, + { + "name": "a", + "kind": { + "type": "u32" + }, + "owner": { + "interface": 2 + } + }, + { + "name": "a", + "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": "bar:name", + "interfaces": { + "i2": 0 + }, + "worlds": { + "w2": 0 + } + }, + { + "name": "baz:name", + "interfaces": { + "i3": 2 + }, + "worlds": { + "w3": 1 + } + }, + { + "name": "foo:name", + "interfaces": { + "i1": 4 + }, + "worlds": { + "w1": 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/multi-package-shared-deps.wit.json b/crates/wit-parser/tests/ui/multi-package-shared-deps.wit.json new file mode 100644 index 0000000000..d3434da0c5 --- /dev/null +++ b/crates/wit-parser/tests/ui/multi-package-shared-deps.wit.json @@ -0,0 +1,83 @@ +{ + "worlds": [ + { + "name": "w-bar", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + }, + "interface-1": { + "interface": { + "id": 1 + } + } + }, + "exports": {}, + "package": 2 + }, + { + "name": "w-qux", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + }, + "interface-1": { + "interface": { + "id": 1 + } + } + }, + "exports": {}, + "package": 3 + } + ], + "interfaces": [ + { + "name": "types", + "types": {}, + "functions": {}, + "package": 0 + }, + { + "name": "types", + "types": {}, + "functions": {}, + "package": 1 + } + ], + "types": [], + "packages": [ + { + "name": "foo:dep1", + "interfaces": { + "types": 0 + }, + "worlds": {} + }, + { + "name": "foo:dep2", + "interfaces": { + "types": 1 + }, + "worlds": {} + }, + { + "name": "foo:bar", + "interfaces": {}, + "worlds": { + "w-bar": 0 + } + }, + { + "name": "foo:qux", + "interfaces": {}, + "worlds": { + "w-qux": 1 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/multi-package-shared-deps/deps/dep1/types.wit b/crates/wit-parser/tests/ui/multi-package-shared-deps/deps/dep1/types.wit new file mode 100644 index 0000000000..4c84420d4f --- /dev/null +++ b/crates/wit-parser/tests/ui/multi-package-shared-deps/deps/dep1/types.wit @@ -0,0 +1,2 @@ +package foo:dep1; +interface types {} diff --git a/crates/wit-parser/tests/ui/multi-package-shared-deps/deps/dep2/types.wit b/crates/wit-parser/tests/ui/multi-package-shared-deps/deps/dep2/types.wit new file mode 100644 index 0000000000..b7cf946632 --- /dev/null +++ b/crates/wit-parser/tests/ui/multi-package-shared-deps/deps/dep2/types.wit @@ -0,0 +1,2 @@ +package foo:dep2; +interface types {} diff --git a/crates/wit-parser/tests/ui/multi-package-shared-deps/packages.wit b/crates/wit-parser/tests/ui/multi-package-shared-deps/packages.wit new file mode 100644 index 0000000000..22d82aa901 --- /dev/null +++ b/crates/wit-parser/tests/ui/multi-package-shared-deps/packages.wit @@ -0,0 +1,13 @@ +package foo:bar { + world w-bar { + import foo:dep1/types; + import foo:dep2/types; + } +} + +package foo:qux { + world w-qux { + import foo:dep1/types; + import foo:dep2/types; + } +} diff --git a/crates/wit-parser/tests/ui/multi-package-transitive-deps.wit.json b/crates/wit-parser/tests/ui/multi-package-transitive-deps.wit.json new file mode 100644 index 0000000000..ece4cdade9 --- /dev/null +++ b/crates/wit-parser/tests/ui/multi-package-transitive-deps.wit.json @@ -0,0 +1,116 @@ +{ + "worlds": [ + { + "name": "w-bar", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + }, + "interface-1": { + "interface": { + "id": 1 + } + } + }, + "exports": {}, + "package": 2 + }, + { + "name": "w-qux", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "exports": {}, + "package": 3 + } + ], + "interfaces": [ + { + "name": "types", + "types": { + "a": 0 + }, + "functions": {}, + "package": 0 + }, + { + "name": "types", + "types": { + "a": 1, + "r": 2 + }, + "functions": {}, + "package": 1 + } + ], + "types": [ + { + "name": "a", + "kind": "resource", + "owner": { + "interface": 0 + } + }, + { + "name": "a", + "kind": { + "type": 0 + }, + "owner": { + "interface": 1 + } + }, + { + "name": "r", + "kind": { + "record": { + "fields": [ + { + "name": "f", + "type": "u8" + } + ] + } + }, + "owner": { + "interface": 1 + } + } + ], + "packages": [ + { + "name": "foo:dep2", + "interfaces": { + "types": 0 + }, + "worlds": {} + }, + { + "name": "foo:dep1", + "interfaces": { + "types": 1 + }, + "worlds": {} + }, + { + "name": "foo:bar", + "interfaces": {}, + "worlds": { + "w-bar": 0 + } + }, + { + "name": "foo:qux", + "interfaces": {}, + "worlds": { + "w-qux": 1 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/multi-package-transitive-deps/deps/dep1/types.wit b/crates/wit-parser/tests/ui/multi-package-transitive-deps/deps/dep1/types.wit new file mode 100644 index 0000000000..825a396472 --- /dev/null +++ b/crates/wit-parser/tests/ui/multi-package-transitive-deps/deps/dep1/types.wit @@ -0,0 +1,9 @@ +package foo:dep1; + +interface types { + use foo:dep2/types.{a}; + + record r { + f: u8, + } +} diff --git a/crates/wit-parser/tests/ui/multi-package-transitive-deps/deps/dep2/types.wit b/crates/wit-parser/tests/ui/multi-package-transitive-deps/deps/dep2/types.wit new file mode 100644 index 0000000000..603efb70b5 --- /dev/null +++ b/crates/wit-parser/tests/ui/multi-package-transitive-deps/deps/dep2/types.wit @@ -0,0 +1,5 @@ +package foo:dep2; + +interface types { + resource a; +} diff --git a/crates/wit-parser/tests/ui/multi-package-transitive-deps/packages.wit b/crates/wit-parser/tests/ui/multi-package-transitive-deps/packages.wit new file mode 100644 index 0000000000..04e04f0af8 --- /dev/null +++ b/crates/wit-parser/tests/ui/multi-package-transitive-deps/packages.wit @@ -0,0 +1,11 @@ +package foo:bar { + world w-bar { + import foo:dep1/types; + } +} + +package foo:qux { + world w-qux { + import foo:dep2/types; + } +} 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..f4c9e7055c --- /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": "bar:name", + "interfaces": { + "i": 0 + }, + "worlds": { + "w": 0 + } + }, + { + "name": "foo: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..de73c51de3 --- /dev/null +++ b/crates/wit-parser/tests/ui/packages-multiple-explicit.wit.json @@ -0,0 +1,130 @@ +{ + "worlds": [ + { + "name": "w2", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + }, + "imp2": { + "interface": { + "id": 1 + } + } + }, + "exports": {}, + "package": 0 + }, + { + "name": "w1", + "imports": { + "interface-2": { + "interface": { + "id": 2 + } + }, + "imp1": { + "interface": { + "id": 3 + } + } + }, + "exports": {}, + "package": 1 + } + ], + "interfaces": [ + { + "name": "i2", + "types": { + "b": 0 + }, + "functions": {}, + "package": 0 + }, + { + "name": null, + "types": { + "b": 1 + }, + "functions": {}, + "package": 0 + }, + { + "name": "i1", + "types": { + "a": 2 + }, + "functions": {}, + "package": 1 + }, + { + "name": null, + "types": { + "a": 3 + }, + "functions": {}, + "package": 1 + } + ], + "types": [ + { + "name": "b", + "kind": { + "type": "u32" + }, + "owner": { + "interface": 0 + } + }, + { + "name": "b", + "kind": { + "type": 0 + }, + "owner": { + "interface": 1 + } + }, + { + "name": "a", + "kind": { + "type": "u32" + }, + "owner": { + "interface": 2 + } + }, + { + "name": "a", + "kind": { + "type": 2 + }, + "owner": { + "interface": 3 + } + } + ], + "packages": [ + { + "name": "bar:name", + "interfaces": { + "i2": 0 + }, + "worlds": { + "w2": 0 + } + }, + { + "name": "foo:name", + "interfaces": { + "i1": 2 + }, + "worlds": { + "w1": 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-package-inline-cycle.wit b/crates/wit-parser/tests/ui/parse-fail/multiple-package-inline-cycle.wit new file mode 100644 index 0000000000..fb0e456fad --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/multiple-package-inline-cycle.wit @@ -0,0 +1,11 @@ +package foo:bar { + interface i { + use foo:qux/i.{}; + } +} + +package foo:qux { + interface i { + use foo:bar/i.{}; + } +} diff --git a/crates/wit-parser/tests/ui/parse-fail/multiple-package-inline-cycle.wit.result b/crates/wit-parser/tests/ui/parse-fail/multiple-package-inline-cycle.wit.result new file mode 100644 index 0000000000..b596cf2d6f --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/multiple-package-inline-cycle.wit.result @@ -0,0 +1,5 @@ +package depends on itself + --> tests/ui/parse-fail/multiple-package-inline-cycle.wit:3:9 + | + 3 | use foo:qux/i.{}; + | ^------ \ 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..3a70a00f83 100644 --- a/crates/wit-smith/src/lib.rs +++ b/crates/wit-smith/src/lib.rs @@ -6,7 +6,7 @@ //! type structures. use arbitrary::{Result, Unstructured}; -use wit_parser::Resolve; +use wit_parser::{Resolve, UnresolvedPackageGroup}; mod config; pub use self::config::Config; @@ -21,8 +21,11 @@ pub fn smith(config: &Config, u: &mut Unstructured<'_>) -> Result> { let mut resolve = Resolve::default(); let mut last = None; for pkg in pkgs { - let unresolved = pkg.sources.parse().unwrap(); - let id = match resolve.push(unresolved) { + let UnresolvedPackageGroup { + mut packages, + source_map, + } = pkg.sources.parse().unwrap(); + let id = match resolve.push(packages.remove(0), &source_map) { Ok(id) => id, Err(e) => { if e.to_string().contains( diff --git a/fuzz/src/roundtrip_wit.rs b/fuzz/src/roundtrip_wit.rs index d5e91c584f..19967992e1 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, &[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..a61e29d4f8 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -11,9 +11,10 @@ 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}; +use wit_parser::{PackageId, Resolve, UnresolvedPackageGroup}; /// WebAssembly wit-based component tooling. #[derive(Parser)] @@ -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)?; + 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 (pkg_ids, _) = resolve.push_dir(&input)?; + return Ok(DecodedWasm::WitPackages(resolve, pkg_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 = UnresolvedPackageGroup::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, &[id])?; + let out_dir = if main.contains(&id) { dir.clone() } else { let dir = dir.join("deps"); @@ -659,7 +664,7 @@ impl WitOpts { } } None => { - let output = printer.print(resolve, main)?; + let output = printer.print(resolve, &main)?; self.output.output(Output::Wat(&output))?; } } @@ -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(); + } +}