From d24d9c468986ff4b80d0462753f6f1622f5eda7e Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 12 Feb 2024 16:04:21 -0600 Subject: [PATCH] Support decoding wasm-encoded WIT packages in wit-parser (#1408) * Make serde optional in `wit-parser` Try to keep its minimal build profile pretty slim. * Move decoding from wit-component to wit-parser * Move text detection from wit-component to wat * Merge `parse_wit_from_path` directly as methods on `Resolve` This commit moves the `wit_component::parse_wit_from_path` function to being a suite of methods directly on `Resolve`. The direct equivalent is now `Resolve::push_path`. * Fix CI configuration * Review comments * More review comments --- .github/workflows/main.yml | 5 + Cargo.lock | 3 + Cargo.toml | 2 +- .../src/bin/failed-instantiations.rs | 2 +- crates/wat/src/lib.rs | 68 ++++++ crates/wit-component/Cargo.toml | 2 +- crates/wit-component/src/encoding.rs | 1 - crates/wit-component/src/encoding/wit/v1.rs | 12 +- crates/wit-component/src/encoding/wit/v2.rs | 12 +- crates/wit-component/src/lib.rs | 129 +--------- crates/wit-component/src/metadata.rs | 48 +--- crates/wit-component/src/validation.rs | 10 +- crates/wit-component/tests/wit.rs | 32 +-- crates/wit-parser/Cargo.toml | 27 ++- .../src/decoding.rs | 72 ++++-- .../src/encoding => wit-parser/src}/docs.rs | 121 +++++---- crates/wit-parser/src/lib.rs | 166 ++++++++----- crates/wit-parser/src/resolve.rs | 229 ++++++++++++++---- crates/wit-parser/tests/.gitignore | 2 + crates/wit-parser/tests/all.rs | 14 +- .../tests/ui/kinds-of-deps.wit.json | 87 +++++++ .../wit-parser/tests/ui/kinds-of-deps/a.wit | 8 + .../tests/ui/kinds-of-deps/deps/b/root.wit | 3 + .../tests/ui/kinds-of-deps/deps/c.wit | 3 + .../tests/ui/kinds-of-deps/deps/d.wat | 6 + .../tests/ui/kinds-of-deps/deps/e.wasm | Bin 0 -> 35 bytes .../tests/ui/parse-fail/bad-pkg1.wit.result | 13 +- .../tests/ui/parse-fail/bad-pkg2.wit.result | 13 +- .../tests/ui/parse-fail/bad-pkg3.wit.result | 13 +- .../tests/ui/parse-fail/bad-pkg4.wit.result | 13 +- .../tests/ui/parse-fail/bad-pkg5.wit.result | 13 +- .../tests/ui/parse-fail/bad-pkg6.wit.result | 13 +- .../ui/parse-fail/bad-resource15.wit.result | 13 +- .../parse-fail/conflicting-package.wit.result | 13 +- .../duplicate-interface2.wit.result | 13 +- .../ui/parse-fail/include-foreign.wit.result | 13 +- .../multiple-package-docs.wit.result | 13 +- .../no-access-to-sibling-use.wit.result | 13 +- .../non-existance-world-include.wit.result | 13 +- .../tests/ui/parse-fail/pkg-cycle.wit.result | 13 +- .../tests/ui/parse-fail/pkg-cycle2.wit.result | 13 +- .../type-and-resource-same-name.wit.result | 13 +- .../ui/parse-fail/unresolved-use10.wit.result | 13 +- .../use-and-include-world.wit.result | 13 +- .../tests/ui/parse-fail/use-world.wit.result | 13 +- .../wit-parser/tests/ui/simple-wasm-text.wat | 6 + .../tests/ui/simple-wasm-text.wit.json | 21 ++ src/bin/wasm-tools/component.rs | 67 ++--- 48 files changed, 869 insertions(+), 536 deletions(-) rename crates/{wit-component => wit-parser}/src/decoding.rs (97%) rename crates/{wit-component/src/encoding => wit-parser/src}/docs.rs (79%) create mode 100644 crates/wit-parser/tests/.gitignore create mode 100644 crates/wit-parser/tests/ui/kinds-of-deps.wit.json create mode 100644 crates/wit-parser/tests/ui/kinds-of-deps/a.wit create mode 100644 crates/wit-parser/tests/ui/kinds-of-deps/deps/b/root.wit create mode 100644 crates/wit-parser/tests/ui/kinds-of-deps/deps/c.wit create mode 100644 crates/wit-parser/tests/ui/kinds-of-deps/deps/d.wat create mode 100644 crates/wit-parser/tests/ui/kinds-of-deps/deps/e.wasm create mode 100644 crates/wit-parser/tests/ui/simple-wasm-text.wat create mode 100644 crates/wit-parser/tests/ui/simple-wasm-text.wit.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 33e706345b..c4b061d282 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -119,6 +119,11 @@ jobs: - run: cargo check --no-default-features --features metadata - run: cargo check --no-default-features --features wit-smith - run: cargo check --no-default-features --features addr2line + - run: cargo check --no-default-features -p wit-parser + - run: cargo check --no-default-features -p wit-parser --features wat + - run: cargo check --no-default-features -p wit-parser --features serde + - run: cargo check --no-default-features -p wit-parser --features decoding + - run: cargo check --no-default-features -p wit-parser --features serde,decoding,wat - run: | if cargo tree -p wasm-smith --no-default-features -e no-dev | grep wasmparser; then echo wasm-smith without default features should not depend on wasmparser diff --git a/Cargo.lock b/Cargo.lock index c62a5d3a19..5b1db84005 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2472,6 +2472,9 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", + "wasmparser 0.121.2", + "wat", + "wit-parser 0.13.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0a99a1eee1..c9697f724a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,7 +113,7 @@ cpp_demangle = { version = "0.4.0", optional = true } # Dependencies of `component` wit-component = { workspace = true, optional = true, features = ['dummy-module', 'wat', 'semver-check'] } -wit-parser = { workspace = true, optional = true } +wit-parser = { workspace = true, optional = true, features = ['decoding', 'wat', 'serde'] } wast = { workspace = true, optional = true } # Dependencies of `metadata` diff --git a/crates/fuzz-stats/src/bin/failed-instantiations.rs b/crates/fuzz-stats/src/bin/failed-instantiations.rs index 1b19ea3eed..d01b235a85 100644 --- a/crates/fuzz-stats/src/bin/failed-instantiations.rs +++ b/crates/fuzz-stats/src/bin/failed-instantiations.rs @@ -106,7 +106,7 @@ impl State { config.gc_enabled = false; let mut wasm = wasm_smith::Module::new(config, &mut u)?; - wasm.ensure_termination(10_000); + wasm.ensure_termination(10_000).unwrap(); let wasm = wasm.to_bytes(); // We install a resource limiter in the store which limits the store to diff --git a/crates/wat/src/lib.rs b/crates/wat/src/lib.rs index c16174602e..9404ad5e0e 100644 --- a/crates/wat/src/lib.rs +++ b/crates/wat/src/lib.rs @@ -71,6 +71,7 @@ use std::borrow::Cow; use std::fmt; use std::path::{Path, PathBuf}; use std::str; +use wast::lexer::{Lexer, TokenKind}; use wast::parser::{self, ParseBuffer}; /// Parses a file on disk as a [WebAssembly Text format][wat] file, or a binary @@ -221,6 +222,73 @@ fn _parse_str(wat: &str) -> Result> { ast.encode().map_err(|e| Error::cvt(e, wat)) } +/// Result of [`Detect::from_bytes`] to indicate what some input bytes look +/// like. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum Detect { + /// The input bytes look like the WebAssembly text format. + WasmText, + /// The input bytes look like the WebAssembly binary format. + WasmBinary, + /// The input bytes don't look like WebAssembly at all. + Unknown, +} + +impl Detect { + /// Detect quickly if supplied bytes represent a Wasm module, + /// whether binary encoded or in WAT-encoded. + /// + /// This briefly lexes past whitespace and comments as a `*.wat` file to see if + /// we can find a left-paren. If that fails then it's probably `*.wit` instead. + /// + /// + /// Examples + /// ``` + /// use wat::Detect; + /// + /// assert_eq!(Detect::from_bytes(r#" + /// (module + /// (type (;0;) (func)) + /// (func (;0;) (type 0) + /// nop + /// ) + /// ) + /// "#), Detect::WasmText); + /// ``` + pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Detect { + if bytes.as_ref().starts_with(b"\0asm") { + return Detect::WasmBinary; + } + let text = match std::str::from_utf8(bytes.as_ref()) { + Ok(s) => s, + Err(_) => return Detect::Unknown, + }; + + let lexer = Lexer::new(text); + let mut iter = lexer.iter(0); + + while let Some(next) = iter.next() { + match next.map(|t| t.kind) { + Ok(TokenKind::Whitespace) + | Ok(TokenKind::BlockComment) + | Ok(TokenKind::LineComment) => {} + Ok(TokenKind::LParen) => return Detect::WasmText, + _ => break, + } + } + + Detect::Unknown + } + + /// Returns whether this is either binary or textual wasm. + pub fn is_wasm(&self) -> bool { + match self { + Detect::WasmText | Detect::WasmBinary => true, + Detect::Unknown => false, + } + } +} + /// A convenience type definition for `Result` where the error is [`Error`] pub type Result = std::result::Result; diff --git a/crates/wit-component/Cargo.toml b/crates/wit-component/Cargo.toml index a8ca36fc2a..4154019b2c 100644 --- a/crates/wit-component/Cargo.toml +++ b/crates/wit-component/Cargo.toml @@ -19,7 +19,7 @@ workspace = true wasmparser = { workspace = true } wasm-encoder = { workspace = true } wasm-metadata = { workspace = true } -wit-parser = { workspace = true } +wit-parser = { workspace = true, features = ['decoding', 'serde'] } anyhow = { workspace = true } log = "0.4.17" bitflags = "2.3.3" diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index 92d03c3501..cb5122a1fb 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -92,7 +92,6 @@ use wit_parser::{ const INDIRECT_TABLE_NAME: &str = "$imports"; -pub mod docs; mod wit; pub use wit::{encode, encode_world}; diff --git a/crates/wit-component/src/encoding/wit/v1.rs b/crates/wit-component/src/encoding/wit/v1.rs index 2c9ea94b1b..5a0a8c6467 100644 --- a/crates/wit-component/src/encoding/wit/v1.rs +++ b/crates/wit-component/src/encoding/wit/v1.rs @@ -1,7 +1,4 @@ -use crate::encoding::{ - docs::PackageDocs, - types::{FunctionKey, ValtypeEncoder}, -}; +use crate::encoding::types::{FunctionKey, ValtypeEncoder}; use anyhow::Result; use indexmap::IndexSet; use std::collections::HashMap; @@ -36,9 +33,10 @@ pub fn encode_component(resolve: &Resolve, package: PackageId) -> Result Result wasm_metadata::Producers { producer } -/// Parse a WIT package from the input `path`. -/// -/// The input `path` can be one of: -/// -/// * A directory containing a WIT package with an optional `deps` directory for -/// any dependent WIT packages it references. -/// * A single standalone WIT file with no dependencies. -/// * A wasm-encoded WIT package as a single file in the wasm binary format. -/// * A wasm-encoded WIT package as a single file in the wasm text format. -/// -/// The `Resolve` containing WIT information along with the `PackageId` of what -/// was parsed is returned if successful. -pub fn parse_wit_from_path( - path: impl AsRef, -) -> Result<(Resolve, wit_parser::PackageId)> { - use anyhow::Context; - - let mut resolver = Resolve::default(); - let id = match path.as_ref() { - // Directories can be directly fed into the resolver - p if p.is_dir() => { - resolver - .push_dir(p) - .with_context(|| { - format!( - "failed to resolve directory while parsing WIT for path [{}]", - p.display() - ) - })? - .0 - } - // Non-directory files (including symlinks) can be either: - // - Wasm modules (binary or WAT) that are WIT packages - // - WIT files - #[cfg(not(feature = "wat"))] - p => { - let file_contents = std::fs::read(p) - .with_context(|| format!("failed to parse WIT from path [{}]", p.display()))?; - match decode(&file_contents)? { - DecodedWasm::Component(..) => { - bail!("specified path is a component, not a wit package") - } - DecodedWasm::WitPackage(resolve, pkg) => return Ok((resolve, pkg)), - } - } - #[cfg(feature = "wat")] - p => { - use wit_parser::UnresolvedPackage; - - let file_contents = std::fs::read(p) - .with_context(|| format!("failed to parse WIT from path [{}]", p.display()))?; - - // Check if the bytes represent a Wasm module (either binary or WAT encoded) - if is_wasm_binary_or_wat(&file_contents) { - let bytes = wat::parse_bytes(&file_contents).map_err(|mut e| { - e.set_path(p); - e - })?; - match decode(&bytes)? { - DecodedWasm::Component(..) => { - bail!("specified path is a component, not a wit package") - } - DecodedWasm::WitPackage(resolve, pkg) => return Ok((resolve, pkg)), - } - } else { - // If the bytes are not a WASM module, they should be WIT that can be parsed - // into a package by the resolver - let text = match std::str::from_utf8(&file_contents) { - Ok(s) => s, - Err(_) => bail!("input file is not valid utf-8"), - }; - let pkg = UnresolvedPackage::parse(p, text)?; - resolver.push(pkg)? - } - } - }; - Ok((resolver, id)) -} - -/// Detect quickly if supplied bytes represent a Wasm module, -/// whether binary encoded or in WAT-encoded. -/// -/// This briefly lexes past whitespace and comments as a `*.wat` file to see if -/// we can find a left-paren. If that fails then it's probably `*.wit` instead. -/// -/// -/// Examples -/// ``` -/// # use wit_component::is_wasm_binary_or_wat; -/// assert!(is_wasm_binary_or_wat(r#" -/// (module -/// (type (;0;) (func)) -/// (func (;0;) (type 0) -/// nop -/// ) -/// ) -/// "#)); -/// ``` -#[cfg(feature = "wat")] -pub fn is_wasm_binary_or_wat(bytes: impl AsRef<[u8]>) -> bool { - use wast::lexer::{Lexer, TokenKind}; - - if bytes.as_ref().starts_with(b"\0asm") { - return true; - } - let text = match std::str::from_utf8(bytes.as_ref()) { - Ok(s) => s, - Err(_) => return true, - }; - - let lexer = Lexer::new(text); - let mut iter = lexer.iter(0); - - while let Some(next) = iter.next() { - match next.map(|t| t.kind) { - Ok(TokenKind::Whitespace) - | Ok(TokenKind::BlockComment) - | Ok(TokenKind::LineComment) => {} - Ok(TokenKind::LParen) => return true, - _ => break, - } - } - - false -} - /// Embed component metadata in a buffer of bytes that contains a Wasm module pub fn embed_component_metadata( bytes: &mut Vec, diff --git a/crates/wit-component/src/metadata.rs b/crates/wit-component/src/metadata.rs index 2ea73155df..2409b02e49 100644 --- a/crates/wit-component/src/metadata.rs +++ b/crates/wit-component/src/metadata.rs @@ -50,10 +50,7 @@ use wasm_encoder::{ ComponentBuilder, ComponentExportKind, ComponentType, ComponentTypeRef, CustomSection, }; use wasm_metadata::Producers; -use wasmparser::types::ComponentAnyTypeId; -use wasmparser::{ - BinaryReader, ComponentExternalKind, Parser, Payload, ValidPayload, Validator, WasmFeatures, -}; +use wasmparser::{BinaryReader, Parser, Payload}; use wit_parser::{Package, PackageName, Resolve, World, WorldId, WorldItem}; const CURRENT_VERSION: u8 = 0x04; @@ -201,33 +198,11 @@ pub fn encode( } fn decode_custom_section(wasm: &[u8]) -> Result<(Resolve, WorldId, StringEncoding)> { - let mut validator = Validator::new_with_features(WasmFeatures::all()); - let mut exports = Vec::new(); - let mut depth = 1; - let mut types = None; + let (resolve, world) = wit_parser::decoding::decode_world(wasm)?; let mut custom_section = None; for payload in Parser::new(0).parse_all(wasm) { - let payload = payload?; - - match validator.payload(&payload)? { - ValidPayload::Ok => {} - ValidPayload::Parser(_) => depth += 1, - ValidPayload::End(t) => { - depth -= 1; - if depth == 0 { - types = Some(t); - } - } - ValidPayload::Func(..) => {} - } - - match payload { - Payload::ComponentExportSection(s) if depth == 1 => { - for export in s { - exports.push(export?); - } - } + match payload? { Payload::CustomSection(s) if s.name() == CUSTOM_SECTION_NAME => { custom_section = Some(s.data()); } @@ -242,23 +217,6 @@ fn decode_custom_section(wasm: &[u8]) -> Result<(Resolve, WorldId, StringEncodin "custom section `{CUSTOM_SECTION_NAME}` uses format {version} but only {CURRENT_VERSION} is supported" ), }; - - if exports.len() != 1 { - bail!("expected one export in component"); - } - if exports[0].kind != ComponentExternalKind::Type { - bail!("expected an export of a type"); - } - if exports[0].ty.is_some() { - bail!("expected an un-ascribed exported type"); - } - let types = types.as_ref().unwrap(); - let ty = match types.component_any_type_at(exports[0].index) { - ComponentAnyTypeId::Component(c) => c, - _ => bail!("expected an exported component type"), - }; - - let (resolve, world) = crate::decoding::decode_world(types, ty)?; Ok((resolve, world, string_encoding)) } diff --git a/crates/wit-component/src/validation.rs b/crates/wit-component/src/validation.rs index aa629f2079..0a76012419 100644 --- a/crates/wit-component/src/validation.rs +++ b/crates/wit-component/src/validation.rs @@ -1,4 +1,3 @@ -use crate::decoding::InterfaceNameExt; use crate::metadata::{Bindgen, ModuleMetadata}; use anyhow::{bail, Context, Result}; use indexmap::{map::Entry, IndexMap, IndexSet}; @@ -10,7 +9,7 @@ use wasmparser::{ }; use wit_parser::{ abi::{AbiVariant, WasmSignature, WasmType}, - Function, InterfaceId, Resolve, TypeDefKind, TypeId, WorldId, WorldItem, WorldKey, + Function, InterfaceId, PackageName, Resolve, TypeDefKind, TypeId, WorldId, WorldItem, WorldKey, }; fn is_canonical_function(name: &str) -> bool { @@ -584,7 +583,12 @@ fn world_key(resolve: &Resolve, name: &str) -> WorldKey { let kebab_name = ComponentName::new(name, 0); let (pkgname, interface) = match kebab_name.as_ref().map(|k| k.kind()) { Ok(ComponentNameKind::Interface(name)) => { - (name.to_package_name(), name.interface().as_str()) + let pkgname = PackageName { + namespace: name.namespace().to_string(), + name: name.package().to_string(), + version: name.version(), + }; + (pkgname, name.interface().as_str()) } _ => return WorldKey::Name(name.to_string()), }; diff --git a/crates/wit-component/tests/wit.rs b/crates/wit-component/tests/wit.rs index 940e94fa15..92d7713288 100644 --- a/crates/wit-component/tests/wit.rs +++ b/crates/wit-component/tests/wit.rs @@ -1,23 +1,15 @@ #![cfg(feature = "wat")] use anyhow::Result; -use wit_component::{is_wasm_binary_or_wat, parse_wit_from_path}; - -const EXAMPLE_MODULE_WAT: &str = r#" -(module - (type (;0;) (func)) - (func (;0;) (type 0) - nop - ) -) -"#; +use wit_parser::Resolve; /// Ensure that parse_wit_from_path works with directories #[test] fn parse_wit_dir() -> Result<()> { drop(env_logger::try_init()); - let (resolver, package_id) = parse_wit_from_path("tests/wit/parse-dir/wit")?; + let mut resolver = Resolve::default(); + let package_id = resolver.push_path("tests/wit/parse-dir/wit")?.0; assert!(resolver .select_world(package_id, "foo-world".into()) .is_ok()); @@ -30,7 +22,10 @@ fn parse_wit_dir() -> Result<()> { fn parse_wit_file() -> Result<()> { drop(env_logger::try_init()); - let (resolver, package_id) = parse_wit_from_path("tests/wit/parse-dir/wit/deps/bar/bar.wit")?; + let mut resolver = Resolve::default(); + let package_id = resolver + .push_path("tests/wit/parse-dir/wit/deps/bar/bar.wit")? + .0; resolver.select_world(package_id, "bar-world".into())?; assert!(resolver .interfaces @@ -45,17 +40,8 @@ fn parse_wit_file() -> Result<()> { fn parse_wit_missing_path() -> Result<()> { drop(env_logger::try_init()); - assert!(parse_wit_from_path("tests/nonexistent/path").is_err()); - - Ok(()) -} - -/// Ensure that is_wasm_binary_or_wat works for binaries -#[test] -fn check_wasm_from_bytes() -> Result<()> { - drop(env_logger::try_init()); - - assert!(is_wasm_binary_or_wat(wat::parse_str(EXAMPLE_MODULE_WAT)?)); + let mut resolver = Resolve::default(); + assert!(resolver.push_path("tests/nonexistent/path").is_err()); Ok(()) } diff --git a/crates/wit-parser/Cargo.toml b/crates/wit-parser/Cargo.toml index f2d9066302..da42af0ed0 100644 --- a/crates/wit-parser/Cargo.toml +++ b/crates/wit-parser/Cargo.toml @@ -18,18 +18,37 @@ workspace = true [dependencies] id-arena = "2" anyhow = { workspace = true } -indexmap = { workspace = true, features = ["serde"] } +indexmap = { workspace = true } unicode-xid = "0.2.2" log = { workspace = true } semver = { workspace = true } -serde = { workspace = true } -serde_derive = { workspace = true } -serde_json = "1.0.105" +serde = { workspace = true, optional = true } +serde_derive = { workspace = true, optional = true } +wasmparser = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } +wat = { workspace = true, optional = true } + +[features] +default = ['serde', 'decoding'] + +# Enables support for `derive(Serialize, Deserialize)` on many structures, such +# as `Resolve`, which can assist when encoding `Resolve` as JSON for example. +serde = ['dep:serde', 'dep:serde_derive', 'indexmap/serde', 'serde_json'] + +# Enables support for decoding WIT from WebAssembly. This can be done to support +# decoding a WIT package encoded as wasm automatically. +decoding = ['dep:wasmparser'] + +# Enables support for parsing the wasm text format in conjunction with the +# `decoding` feature. +wat = ['decoding', 'dep:wat'] [dev-dependencies] rayon = "1" env_logger = { workspace = true } pretty_assertions = { workspace = true } +serde_json = { workspace = true } +wit-parser = { path = '.', features = ['serde', 'wat'] } [[test]] name = "all" diff --git a/crates/wit-component/src/decoding.rs b/crates/wit-parser/src/decoding.rs similarity index 97% rename from crates/wit-component/src/decoding.rs rename to crates/wit-parser/src/decoding.rs index 48964e971b..0b8679ec95 100644 --- a/crates/wit-component/src/decoding.rs +++ b/crates/wit-parser/src/decoding.rs @@ -1,3 +1,5 @@ +use crate::docs::PackageDocs; +use crate::*; use anyhow::{anyhow, bail, Context, Result}; use indexmap::{IndexMap, IndexSet}; use std::mem; @@ -5,12 +7,11 @@ use std::{collections::HashMap, io::Read}; use wasmparser::Chunk; use wasmparser::{ names::{ComponentName, ComponentNameKind}, - types, ComponentExternalKind, Parser, Payload, PrimitiveValType, ValidPayload, Validator, + types, + types::ComponentAnyTypeId, + ComponentExternalKind, Parser, Payload, PrimitiveValType, ValidPayload, Validator, WasmFeatures, }; -use wit_parser::*; - -use crate::encoding::docs::{PackageDocs, PACKAGE_DOCS_SECTION_NAME}; /// Represents information about a decoded WebAssembly component. struct ComponentInfo { @@ -48,7 +49,7 @@ impl ComponentInfo { let mut externs = Vec::new(); let mut depth = 1; let mut types = None; - let mut package_docs = None; + let mut _package_docs = None; let mut cur = Parser::new(0); let mut eof = false; let mut stack = Vec::new(); @@ -110,11 +111,12 @@ impl ComponentInfo { )); } } - Payload::CustomSection(s) if s.name() == PACKAGE_DOCS_SECTION_NAME => { - if package_docs.is_some() { - bail!("multiple {PACKAGE_DOCS_SECTION_NAME:?} sections"); + #[cfg(feature = "serde")] + Payload::CustomSection(s) if s.name() == PackageDocs::SECTION_NAME => { + if _package_docs.is_some() { + bail!("multiple {:?} sections", PackageDocs::SECTION_NAME); } - package_docs = Some(PackageDocs::decode(s.data())?); + _package_docs = Some(PackageDocs::decode(s.data())?); } Payload::ModuleSection { parser, .. } | Payload::ComponentSection { parser, .. } => { @@ -139,7 +141,7 @@ impl ComponentInfo { Ok(Self { types: types.unwrap(), externs, - package_docs, + package_docs: _package_docs, }) } @@ -412,10 +414,52 @@ pub fn decode(bytes: &[u8]) -> Result { /// itself imports nothing and exports a single component, and the single /// component export represents the world. The name of the export is also the /// name of the package/world/etc. -pub(crate) fn decode_world( - types: &types::Types, - world: types::ComponentTypeId, -) -> Result<(Resolve, WorldId)> { +pub fn decode_world(wasm: &[u8]) -> Result<(Resolve, WorldId)> { + let mut validator = Validator::new_with_features(WasmFeatures::all()); + let mut exports = Vec::new(); + let mut depth = 1; + let mut types = None; + + for payload in Parser::new(0).parse_all(wasm) { + let payload = payload?; + + match validator.payload(&payload)? { + ValidPayload::Ok => {} + ValidPayload::Parser(_) => depth += 1, + ValidPayload::End(t) => { + depth -= 1; + if depth == 0 { + types = Some(t); + } + } + ValidPayload::Func(..) => {} + } + + match payload { + Payload::ComponentExportSection(s) if depth == 1 => { + for export in s { + exports.push(export?); + } + } + _ => {} + } + } + + if exports.len() != 1 { + bail!("expected one export in component"); + } + if exports[0].kind != ComponentExternalKind::Type { + bail!("expected an export of a type"); + } + if exports[0].ty.is_some() { + bail!("expected an un-ascribed exported type"); + } + let types = types.as_ref().unwrap(); + let world = match types.component_any_type_at(exports[0].index) { + ComponentAnyTypeId::Component(c) => c, + _ => bail!("expected an exported component type"), + }; + let mut decoder = WitPackageDecoder::new(types); let mut interfaces = IndexMap::new(); let mut worlds = IndexMap::new(); diff --git a/crates/wit-component/src/encoding/docs.rs b/crates/wit-parser/src/docs.rs similarity index 79% rename from crates/wit-component/src/encoding/docs.rs rename to crates/wit-parser/src/docs.rs index accdbc2db3..a21ba185be 100644 --- a/crates/wit-component/src/encoding/docs.rs +++ b/crates/wit-parser/src/docs.rs @@ -1,27 +1,40 @@ +use crate::{ + Docs, InterfaceId, PackageId, Resolve, TypeDefKind, TypeId, WorldId, WorldItem, WorldKey, +}; use anyhow::{bail, Result}; use indexmap::IndexMap; +#[cfg(feature = "serde")] use serde_derive::{Deserialize, Serialize}; -use wasm_encoder::{CustomSection, Encode}; -use wit_parser::{Docs, InterfaceId, PackageId, Resolve, TypeId, WorldId, WorldItem, WorldKey}; type StringMap = IndexMap; -pub const PACKAGE_DOCS_SECTION_NAME: &str = "package-docs"; +#[cfg(feature = "serde")] const PACKAGE_DOCS_SECTION_VERSION: u8 = 0; /// Represents serializable doc comments parsed from a WIT package. -#[derive(Serialize, Deserialize)] -#[serde(deny_unknown_fields)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] pub struct PackageDocs { - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Option::is_none") + )] docs: Option, - #[serde(default, skip_serializing_if = "StringMap::is_empty")] + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "StringMap::is_empty") + )] worlds: StringMap, - #[serde(default, skip_serializing_if = "StringMap::is_empty")] + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "StringMap::is_empty") + )] interfaces: StringMap, } impl PackageDocs { + pub const SECTION_NAME: &'static str = "package-docs"; + /// Extract package docs for the given package. pub fn extract(resolve: &Resolve, package: PackageId) -> Self { let package = &resolve.packages[package]; @@ -69,21 +82,16 @@ impl PackageDocs { } /// Encode package docs as a package-docs custom section. - pub fn raw_custom_section(&self) -> Result> { + #[cfg(feature = "serde")] + pub fn encode(&self) -> Result> { // Version byte (0), followed by JSON encoding of docs let mut data = vec![PACKAGE_DOCS_SECTION_VERSION]; serde_json::to_writer(&mut data, self)?; - - let mut raw_section = vec![]; - CustomSection { - name: PACKAGE_DOCS_SECTION_NAME.into(), - data: data.into(), - } - .encode(&mut raw_section); - Ok(raw_section) + Ok(data) } /// Decode package docs from package-docs custom section content. + #[cfg(feature = "serde")] pub fn decode(data: &[u8]) -> Result { let version = data.first(); if version != Some(&PACKAGE_DOCS_SECTION_VERSION) { @@ -93,16 +101,28 @@ impl PackageDocs { } } -#[derive(Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct WorldDocs { - #[serde(default, skip_serializing_if = "Option::is_none")] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +struct WorldDocs { + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Option::is_none") + )] docs: Option, - #[serde(default, skip_serializing_if = "StringMap::is_empty")] + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "StringMap::is_empty") + )] interfaces: StringMap, - #[serde(default, skip_serializing_if = "StringMap::is_empty")] + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "StringMap::is_empty") + )] types: StringMap, - #[serde(default, skip_serializing_if = "StringMap::is_empty")] + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "StringMap::is_empty") + )] funcs: StringMap, } @@ -195,14 +215,23 @@ impl WorldDocs { } } -#[derive(Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct InterfaceDocs { - #[serde(default, skip_serializing_if = "Option::is_none")] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +struct InterfaceDocs { + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Option::is_none") + )] docs: Option, - #[serde(default, skip_serializing_if = "StringMap::is_empty")] + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "StringMap::is_empty") + )] funcs: StringMap, - #[serde(default, skip_serializing_if = "StringMap::is_empty")] + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "StringMap::is_empty") + )] types: StringMap, } @@ -254,13 +283,19 @@ impl InterfaceDocs { } } -#[derive(Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct TypeDocs { - #[serde(default, skip_serializing_if = "Option::is_none")] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +struct TypeDocs { + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Option::is_none") + )] docs: Option, // record fields, variant cases, etc. - #[serde(default, skip_serializing_if = "StringMap::is_empty")] + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "StringMap::is_empty") + )] items: StringMap, } @@ -277,16 +312,16 @@ impl TypeDocs { } let ty = &resolve.types[id]; let items = match &ty.kind { - wit_parser::TypeDefKind::Record(record) => { + TypeDefKind::Record(record) => { extract_items(&record.fields, |item| (&item.name, &item.docs)) } - wit_parser::TypeDefKind::Flags(flags) => { + TypeDefKind::Flags(flags) => { extract_items(&flags.flags, |item| (&item.name, &item.docs)) } - wit_parser::TypeDefKind::Variant(variant) => { + TypeDefKind::Variant(variant) => { extract_items(&variant.cases, |item| (&item.name, &item.docs)) } - wit_parser::TypeDefKind::Enum(enum_) => { + TypeDefKind::Enum(enum_) => { extract_items(&enum_.cases, |item| (&item.name, &item.docs)) } // other types don't have inner items @@ -303,16 +338,16 @@ impl TypeDocs { let ty = &mut resolve.types[id]; if !self.items.is_empty() { match &mut ty.kind { - wit_parser::TypeDefKind::Record(record) => { + TypeDefKind::Record(record) => { self.inject_items(&mut record.fields, |item| (&item.name, &mut item.docs))? } - wit_parser::TypeDefKind::Flags(flags) => { + TypeDefKind::Flags(flags) => { self.inject_items(&mut flags.flags, |item| (&item.name, &mut item.docs))? } - wit_parser::TypeDefKind::Variant(variant) => { + TypeDefKind::Variant(variant) => { self.inject_items(&mut variant.cases, |item| (&item.name, &mut item.docs))? } - wit_parser::TypeDefKind::Enum(enum_) => { + TypeDefKind::Enum(enum_) => { self.inject_items(&mut enum_.cases, |item| (&item.name, &mut item.docs))? } _ => { diff --git a/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs index 100b8e4976..1905426efb 100644 --- a/crates/wit-parser/src/lib.rs +++ b/crates/wit-parser/src/lib.rs @@ -2,11 +2,17 @@ use anyhow::{Context, Result}; use id_arena::{Arena, Id}; use indexmap::IndexMap; use semver::Version; -use serde_derive::Serialize; use std::borrow::Cow; use std::fmt; use std::path::Path; +#[cfg(feature = "decoding")] +pub mod decoding; +#[cfg(feature = "decoding")] +mod docs; +#[cfg(feature = "decoding")] +pub use docs::PackageDocs; + pub mod abi; mod ast; use ast::lex::Span; @@ -17,7 +23,12 @@ mod resolve; pub use resolve::{Package, PackageId, Remap, Resolve}; mod live; pub use live::LiveTypes; + +#[cfg(feature = "serde")] +use serde_derive::Serialize; +#[cfg(feature = "serde")] mod serde_; +#[cfg(feature = "serde")] use serde_::{ serialize_anon_result, serialize_id, serialize_id_map, serialize_none, serialize_optional_id, serialize_params, @@ -107,12 +118,13 @@ pub struct UnresolvedPackage { required_resource_types: Vec<(TypeId, Span)>, } -#[derive(Debug, Copy, Clone, Serialize)] -#[serde(rename_all = "lowercase")] +#[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] pub enum AstItem { - #[serde(serialize_with = "serialize_id")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))] Interface(InterfaceId), - #[serde(serialize_with = "serialize_id")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))] World(WorldId), } @@ -121,8 +133,9 @@ pub enum AstItem { /// /// This is directly encoded as an "ID" in the binary component representation /// with an interfaced tacked on as well. -#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize)] -#[serde(into = "String")] +#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize))] +#[cfg_attr(feature = "serde", serde(into = "String"))] pub struct PackageName { /// A namespace such as `wasi` in `wasi:foo/bar` pub namespace: String, @@ -247,7 +260,8 @@ impl UnresolvedPackage { } } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct World { /// The WIT identifier name of this world. pub name: String, @@ -259,19 +273,19 @@ pub struct World { pub exports: IndexMap, /// The package that owns this world. - #[serde(serialize_with = "serialize_optional_id")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_optional_id"))] pub package: Option, /// Documentation associated with this world declaration. - #[serde(skip_serializing_if = "Docs::is_empty")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Docs::is_empty"))] pub docs: Docs, /// All the included worlds from this world. Empty if this is fully resolved - #[serde(skip)] + #[cfg_attr(feature = "serde", serde(skip))] pub includes: Vec, /// All the included worlds names. Empty if this is fully resolved - #[serde(skip)] + #[cfg_attr(feature = "serde", serde(skip))] pub include_names: Vec>, } @@ -286,8 +300,9 @@ pub struct IncludeName { /// The key to the import/export maps of a world. Either a kebab-name or a /// unique interface. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] -#[serde(into = "String")] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize))] +#[cfg_attr(feature = "serde", serde(into = "String"))] pub enum WorldKey { /// A kebab-name. Name(String), @@ -315,12 +330,13 @@ impl WorldKey { } } -#[derive(Debug, Clone, PartialEq, Serialize)] -#[serde(rename_all = "lowercase")] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] pub enum WorldItem { /// An interface is being imported or exported from a world, indicating that /// it's a namespace of functions. - #[serde(serialize_with = "serialize_id")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))] Interface(InterfaceId), /// A function is being directly imported or exported from this world. @@ -329,11 +345,12 @@ pub enum WorldItem { /// A type is being exported from this world. /// /// Note that types are never imported into worlds at this time. - #[serde(serialize_with = "serialize_id")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))] Type(TypeId), } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct Interface { /// Optionally listed name of this interface. /// @@ -344,32 +361,34 @@ pub struct Interface { /// /// Export names are listed within the types themselves. Note that the /// export name here matches the name listed in the `TypeDef`. - #[serde(serialize_with = "serialize_id_map")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id_map"))] pub types: IndexMap, /// Exported functions from this interface. pub functions: IndexMap, /// Documentation associated with this interface. - #[serde(skip_serializing_if = "Docs::is_empty")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Docs::is_empty"))] pub docs: Docs, /// The package that owns this interface. - #[serde(serialize_with = "serialize_optional_id")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_optional_id"))] pub package: Option, } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct TypeDef { pub name: Option, pub kind: TypeDefKind, pub owner: TypeOwner, - #[serde(skip_serializing_if = "Docs::is_empty")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Docs::is_empty"))] pub docs: Docs, } -#[derive(Debug, Clone, PartialEq, Serialize)] -#[serde(rename_all = "lowercase")] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] pub enum TypeDefKind { Record(Record), Resource, @@ -417,27 +436,29 @@ impl TypeDefKind { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize)] -#[serde(rename_all = "lowercase")] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] pub enum TypeOwner { /// This type was defined within a `world` block. - #[serde(serialize_with = "serialize_id")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))] World(WorldId), /// This type was defined within an `interface` block. - #[serde(serialize_with = "serialize_id")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))] Interface(InterfaceId), /// This type wasn't inherently defined anywhere, such as a `list`, which /// doesn't need an owner. - #[serde(untagged, serialize_with = "serialize_none")] + #[cfg_attr(feature = "serde", serde(untagged, serialize_with = "serialize_none"))] None, } -#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Serialize)] -#[serde(rename_all = "lowercase")] +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] pub enum Handle { - #[serde(serialize_with = "serialize_id")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))] Own(TypeId), - #[serde(serialize_with = "serialize_id")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))] Borrow(TypeId), } @@ -467,29 +488,33 @@ pub enum Int { U64, } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct Record { pub fields: Vec, } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct Field { pub name: String, - #[serde(rename = "type")] + #[cfg_attr(feature = "serde", serde(rename = "type"))] pub ty: Type, - #[serde(skip_serializing_if = "Docs::is_empty")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Docs::is_empty"))] pub docs: Docs, } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct Flags { pub flags: Vec, } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct Flag { pub name: String, - #[serde(skip_serializing_if = "Docs::is_empty")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Docs::is_empty"))] pub docs: Docs, } @@ -521,22 +546,25 @@ impl FlagsRepr { } } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct Tuple { pub types: Vec, } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct Variant { pub cases: Vec, } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct Case { pub name: String, - #[serde(rename = "type")] + #[cfg_attr(feature = "serde", serde(rename = "type"))] pub ty: Option, - #[serde(skip_serializing_if = "Docs::is_empty")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Docs::is_empty"))] pub docs: Docs, } @@ -551,15 +579,17 @@ impl Variant { } } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct Enum { pub cases: Vec, } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct EnumCase { pub name: String, - #[serde(skip_serializing_if = "Docs::is_empty")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Docs::is_empty"))] pub docs: Docs, } @@ -574,19 +604,22 @@ impl Enum { } } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct Result_ { pub ok: Option, pub err: Option, } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct Stream { pub element: Option, pub end: Option, } -#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize)] +#[derive(Clone, Default, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct Docs { pub contents: Option, } @@ -599,12 +632,13 @@ impl Docs { pub type Params = Vec<(String, Type)>; -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] -#[serde(untagged)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize))] +#[cfg_attr(feature = "serde", serde(untagged))] pub enum Results { - #[serde(serialize_with = "serialize_params")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_params"))] Named(Params), - #[serde(serialize_with = "serialize_anon_result")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_anon_result"))] Anon(Type), } @@ -667,26 +701,28 @@ impl Results { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct Function { pub name: String, pub kind: FunctionKind, - #[serde(serialize_with = "serialize_params")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_params"))] pub params: Params, pub results: Results, - #[serde(skip_serializing_if = "Docs::is_empty")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Docs::is_empty"))] pub docs: Docs, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -#[serde(rename_all = "lowercase")] +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] pub enum FunctionKind { Freestanding, - #[serde(serialize_with = "serialize_id")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))] Method(TypeId), - #[serde(serialize_with = "serialize_id")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))] Static(TypeId), - #[serde(serialize_with = "serialize_id")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))] Constructor(TypeId), } diff --git a/crates/wit-parser/src/resolve.rs b/crates/wit-parser/src/resolve.rs index d0d37097b5..273c183626 100644 --- a/crates/wit-parser/src/resolve.rs +++ b/crates/wit-parser/src/resolve.rs @@ -1,5 +1,6 @@ use crate::ast::lex::Span; use crate::ast::{parse_use_path, AstUsePath}; +#[cfg(feature = "serde")] use crate::serde_::{serialize_arena, serialize_id_map}; use crate::{ AstItem, Docs, Error, Function, FunctionKind, Handle, IncludeName, Interface, InterfaceId, @@ -9,6 +10,7 @@ use crate::{ use anyhow::{anyhow, bail, Context, Result}; use id_arena::{Arena, Id}; use indexmap::{IndexMap, IndexSet}; +#[cfg(feature = "serde")] use serde_derive::Serialize; use std::collections::{BTreeMap, HashMap, HashSet}; use std::mem; @@ -27,20 +29,21 @@ use std::path::{Path, PathBuf}; /// /// Each item in a `Resolve` has a parent link to trace it back to the original /// package as necessary. -#[derive(Default, Clone, Debug, Serialize)] +#[derive(Default, Clone, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct Resolve { /// All knowns worlds within this `Resolve`. /// /// Each world points at a `PackageId` which is stored below. No ordering is /// guaranteed between this list of worlds. - #[serde(serialize_with = "serialize_arena")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_arena"))] pub worlds: Arena, /// All knowns interfaces within this `Resolve`. /// /// Each interface points at a `PackageId` which is stored below. No /// ordering is guaranteed between this list of interfaces. - #[serde(serialize_with = "serialize_arena")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_arena"))] pub interfaces: Arena, /// All knowns types within this `Resolve`. @@ -48,18 +51,18 @@ pub struct Resolve { /// Types are topologically sorted such that any type referenced from one /// type is guaranteed to be defined previously. Otherwise though these are /// not sorted by interface for example. - #[serde(serialize_with = "serialize_arena")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_arena"))] pub types: Arena, /// All knowns packages within this `Resolve`. /// /// This list of packages is not sorted. Sorted packages can be queried /// through [`Resolve::topological_packages`]. - #[serde(serialize_with = "serialize_arena")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_arena"))] pub packages: Arena, /// A map of package names to the ID of the package with that name. - #[serde(skip)] + #[cfg_attr(feature = "serde", serde(skip))] pub package_names: IndexMap, } @@ -68,40 +71,90 @@ pub struct Resolve { /// A package is a collection of interfaces and worlds. Packages additionally /// have a unique identifier that affects generated components and uniquely /// identifiers this particular package. -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct Package { /// A unique name corresponding to this package. pub name: PackageName, /// Documentation associated with this package. - #[serde(skip_serializing_if = "Docs::is_empty")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Docs::is_empty"))] pub docs: Docs, /// All interfaces contained in this packaged, keyed by the interface's /// name. - #[serde(serialize_with = "serialize_id_map")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id_map"))] pub interfaces: IndexMap, /// All worlds contained in this package, keyed by the world's name. - #[serde(serialize_with = "serialize_id_map")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id_map"))] pub worlds: IndexMap, } pub type PackageId = Id; +enum ParsedFile { + #[cfg(feature = "decoding")] + Package(PackageId), + Unresolved(UnresolvedPackage), +} + impl Resolve { /// Creates a new [`Resolve`] with no packages/items inside of it. pub fn new() -> Resolve { Resolve::default() } + /// Parse a WIT package from the input `path`. + /// + /// The input `path` can be one of: + /// + /// * A directory containing a WIT package with an optional `deps` directory + /// for any dependent WIT packages it references. + /// * A single standalone WIT file depending on what's already in `Resolve`. + /// * A wasm-encoded WIT package as a single file in the wasm binary format. + /// * A wasm-encoded WIT package as a single file in the wasm text format. + /// + /// The `PackageId` of the parsed package is returned. For more information + /// see [`Resolve::push_dir`] and [`Resolve::push_file`]. This method will + /// automatically call the appropriate method based on what kind of + /// filesystem entry `path` is. + /// + /// 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)> { + self._push_path(path.as_ref()) + } + + fn _push_path(&mut self, path: &Path) -> Result<(PackageId, Vec)> { + if path.is_dir() { + self.push_dir(path).with_context(|| { + format!( + "failed to resolve directory while parsing WIT for path [{}]", + path.display() + ) + }) + } else { + let id = self.push_file(path)?; + Ok((id, vec![path.to_path_buf()])) + } + } + /// Parses the filesystem directory at `path` as a WIT package and returns /// the fully resolved [`PackageId`] as a result. /// - /// Dependencies referenced by the WIT package at `path` will be loaded from - /// a `deps/..` directory under `path`. All directories under `deps/` will - /// be parsed as a WIT package. The directory name containing each package - /// is not used as each package is otherwise self-identifying. + /// The directory itself is parsed with [`UnresolvedPackage::parse_dir`] + /// which has more information on the layout of the directory. This method, + /// however, additionally supports an optional `deps` dir where dependencies + /// can be located. + /// + /// All entries in the `deps` directory are inspected and parsed as follows: + /// + /// * Any directories inside of `deps` are assumed to be another WIT package + /// and are parsed with [`UnresolvedPackage::parse_dir`]. + /// * WIT files (`*.wit`) are parsed with [`UnresolvedPackage::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 /// `path`, along with a list of all paths that were consumed during parsing @@ -111,7 +164,8 @@ impl Resolve { .with_context(|| format!("failed to parse package: {}", path.display()))?; let deps = path.join("deps"); - let mut deps = parse_deps_dir(&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 @@ -138,34 +192,6 @@ impl Resolve { return Ok((last.unwrap(), files)); - fn parse_deps_dir(path: &Path) -> Result> { - let mut ret = BTreeMap::new(); - // If there's no `deps` dir, then there's no deps, so return the - // empty set. - if !path.exists() { - return Ok(ret); - } - for dep in path.read_dir().context("failed to read directory")? { - let dep = dep.context("failed to read directory iterator")?; - let path = dep.path(); - - // Files in deps dir are ignored for now to avoid accidentally - // including things like `.DS_Store` files in the call below to - // `parse_dir`. - if path.is_file() { - continue; - } - - let pkg = UnresolvedPackage::parse_dir(&path) - .with_context(|| format!("failed to parse package: {}", path.display()))?; - let prev = ret.insert(pkg.name.clone(), pkg); - if let Some(prev) = prev { - bail!("duplicate definitions of package `{}` found", prev.name); - } - } - Ok(ret) - } - fn visit<'a>( pkg: &'a UnresolvedPackage, deps: &'a BTreeMap, @@ -184,12 +210,10 @@ impl Resolve { msg: format!("package depends on itself"), }); } - let dep = deps.get(dep).ok_or_else(|| Error { - span, - msg: format!("failed to find package `{dep}` in `deps` directory"), - })?; - visit(dep, deps, order, visiting)?; - assert!(visiting.remove(&dep.name)); + if let Some(dep) = deps.get(dep) { + visit(dep, deps, order, visiting)?; + } + assert!(visiting.remove(dep)); } assert!(order.insert(pkg.name.clone())); Ok(()) @@ -197,6 +221,113 @@ impl Resolve { } } + fn parse_deps_dir(&mut self, path: &Path) -> Result> { + let mut ret = BTreeMap::new(); + // If there's no `deps` dir, then there's no deps, so return the + // empty set. + if !path.exists() { + return Ok(ret); + } + for dep in path.read_dir().context("failed to read directory")? { + let dep = dep.context("failed to read directory iterator")?; + let path = dep.path(); + + let pkg = if dep.file_type()?.is_dir() { + // If this entry is 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()))? + } 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, + }, + + // 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) + } + + /// Parses the contents of `path` from the filesystem and pushes the result + /// into this `Resolve`. + /// + /// The `path` referenced here can be one of: + /// + /// * A WIT file. Note that in this case this single WIT file will be the + /// entire package and any dependencies it has must already be in `self`. + /// * A WIT package encoded as WebAssembly, either in text or binary form. + /// In this the package and all of its dependencies are automatically + /// inserted into `self`. + /// + /// In both situations the `PackageId` of the resulting resolved package is + /// returned from this method. + 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), + } + } + + fn _push_file(&mut self, path: &Path) -> Result { + let contents = std::fs::read(path) + .with_context(|| format!("failed to read path for WIT [{}]", path.display()))?; + + // If decoding is enabled at compile time then try to see if this is a + // wasm file. + #[cfg(feature = "decoding")] + { + use crate::decoding::{decode, DecodedWasm}; + + #[cfg(feature = "wat")] + let is_wasm = wat::Detect::from_bytes(&contents).is_wasm(); + #[cfg(not(feature = "wat"))] + let is_wasm = wasmparser::Parser::is_component(&contents); + + if is_wasm { + #[cfg(feature = "wat")] + let contents = wat::parse_bytes(&contents).map_err(|mut e| { + e.set_path(path); + e + })?; + + match decode(&contents)? { + DecodedWasm::Component(..) => { + bail!("found an actual component instead of an encoded WIT package in wasm") + } + DecodedWasm::WitPackage(resolve, pkg) => { + let remap = self.merge(resolve)?; + return Ok(ParsedFile::Package(remap.packages[pkg.index()])); + } + } + } + } + + // If this wasn't a wasm file then assume it's a WIT file. + let text = match std::str::from_utf8(&contents) { + Ok(s) => s, + Err(_) => bail!("input file is not valid utf-8 [{}]", path.display()), + }; + let pkg = UnresolvedPackage::parse(path, text)?; + Ok(ParsedFile::Unresolved(pkg)) + } + /// Appends a new [`UnresolvedPackage`] to this [`Resolve`], creating a /// fully resolved package with no dangling references. /// diff --git a/crates/wit-parser/tests/.gitignore b/crates/wit-parser/tests/.gitignore new file mode 100644 index 0000000000..9365962f41 --- /dev/null +++ b/crates/wit-parser/tests/.gitignore @@ -0,0 +1,2 @@ +!*.wasm +!*.wat diff --git a/crates/wit-parser/tests/all.rs b/crates/wit-parser/tests/all.rs index dfd294f6ac..096192d7dc 100644 --- a/crates/wit-parser/tests/all.rs +++ b/crates/wit-parser/tests/all.rs @@ -87,8 +87,7 @@ fn find_tests() -> Vec { } match path.extension().and_then(|s| s.to_str()) { - Some("md") => {} - Some("wit") => {} + Some("md") | Some("wit") | Some("wat") | Some("wasm") => {} _ => continue, } tests.push(path); @@ -103,16 +102,7 @@ struct Runner<'a> { impl Runner<'_> { fn run(&mut self, test: &Path) -> Result<()> { let mut resolve = Resolve::new(); - let result = if test.is_dir() { - resolve.push_dir(test).map(|(id, _)| id) - } else { - let mut map = SourceMap::new(); - map.set_require_semicolons(true); - map.push_file(test) - .and_then(|()| map.parse()) - .and_then(|p| resolve.push(p)) - }; - + let result = resolve.push_path(test); let result = if test.iter().any(|s| s == "parse-fail") { match result { Ok(_) => bail!("expected test to not parse but it did"), diff --git a/crates/wit-parser/tests/ui/kinds-of-deps.wit.json b/crates/wit-parser/tests/ui/kinds-of-deps.wit.json new file mode 100644 index 0000000000..07f01a7cba --- /dev/null +++ b/crates/wit-parser/tests/ui/kinds-of-deps.wit.json @@ -0,0 +1,87 @@ +{ + "worlds": [ + { + "name": "a", + "imports": { + "interface-2": { + "interface": 2 + }, + "interface-3": { + "interface": 3 + }, + "interface-0": { + "interface": 0 + }, + "interface-1": { + "interface": 1 + } + }, + "exports": {}, + "package": 4 + } + ], + "interfaces": [ + { + "name": "d", + "types": {}, + "functions": {}, + "package": 0 + }, + { + "name": "e", + "types": {}, + "functions": {}, + "package": 1 + }, + { + "name": "b", + "types": {}, + "functions": {}, + "package": 2 + }, + { + "name": "c", + "types": {}, + "functions": {}, + "package": 3 + } + ], + "types": [], + "packages": [ + { + "name": "d:d", + "interfaces": { + "d": 0 + }, + "worlds": {} + }, + { + "name": "e:e", + "interfaces": { + "e": 1 + }, + "worlds": {} + }, + { + "name": "b:b", + "interfaces": { + "b": 2 + }, + "worlds": {} + }, + { + "name": "c:c", + "interfaces": { + "c": 3 + }, + "worlds": {} + }, + { + "name": "a:a", + "interfaces": {}, + "worlds": { + "a": 0 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/kinds-of-deps/a.wit b/crates/wit-parser/tests/ui/kinds-of-deps/a.wit new file mode 100644 index 0000000000..77f3de602b --- /dev/null +++ b/crates/wit-parser/tests/ui/kinds-of-deps/a.wit @@ -0,0 +1,8 @@ +package a:a; + +world a { + import b:b/b; + import c:c/c; + import d:d/d; + import e:e/e; +} diff --git a/crates/wit-parser/tests/ui/kinds-of-deps/deps/b/root.wit b/crates/wit-parser/tests/ui/kinds-of-deps/deps/b/root.wit new file mode 100644 index 0000000000..ed4c65f738 --- /dev/null +++ b/crates/wit-parser/tests/ui/kinds-of-deps/deps/b/root.wit @@ -0,0 +1,3 @@ +package b:b; + +interface b {} diff --git a/crates/wit-parser/tests/ui/kinds-of-deps/deps/c.wit b/crates/wit-parser/tests/ui/kinds-of-deps/deps/c.wit new file mode 100644 index 0000000000..a5d36e07b4 --- /dev/null +++ b/crates/wit-parser/tests/ui/kinds-of-deps/deps/c.wit @@ -0,0 +1,3 @@ +package c:c; + +interface c {} diff --git a/crates/wit-parser/tests/ui/kinds-of-deps/deps/d.wat b/crates/wit-parser/tests/ui/kinds-of-deps/deps/d.wat new file mode 100644 index 0000000000..18fdfa279c --- /dev/null +++ b/crates/wit-parser/tests/ui/kinds-of-deps/deps/d.wat @@ -0,0 +1,6 @@ +(component + (type $d (component + (export "d:d/d" (instance)) + )) + (export "d" (type $d)) +) diff --git a/crates/wit-parser/tests/ui/kinds-of-deps/deps/e.wasm b/crates/wit-parser/tests/ui/kinds-of-deps/deps/e.wasm new file mode 100644 index 0000000000000000000000000000000000000000..6254d1bd064b1e94b89a02e2d24a9a7b8cf448a0 GIT binary patch literal 35 qcmZQbEY9U+U}RtyV02_+bYfs(WKFe7)lX$*;AUrJU`%CZU;qG8?F113 literal 0 HcmV?d00001 diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-pkg1.wit.result b/crates/wit-parser/tests/ui/parse-fail/bad-pkg1.wit.result index 85cc50ed99..e366b72989 100644 --- a/crates/wit-parser/tests/ui/parse-fail/bad-pkg1.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/bad-pkg1.wit.result @@ -1,8 +1,9 @@ -failed to parse package: tests/ui/parse-fail/bad-pkg1 +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/bad-pkg1] Caused by: - interface or world `nonexistent` not found in package - --> tests/ui/parse-fail/bad-pkg1/root.wit:4:7 - | - 4 | use nonexistent.{}; - | ^---------- \ No newline at end of file + 0: failed to parse package: tests/ui/parse-fail/bad-pkg1 + 1: interface or world `nonexistent` not found in package + --> tests/ui/parse-fail/bad-pkg1/root.wit:4:7 + | + 4 | use nonexistent.{}; + | ^---------- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-pkg2.wit.result b/crates/wit-parser/tests/ui/parse-fail/bad-pkg2.wit.result index 57f45a0779..3827d5db46 100644 --- a/crates/wit-parser/tests/ui/parse-fail/bad-pkg2.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/bad-pkg2.wit.result @@ -1,5 +1,8 @@ -interface not found in package - --> tests/ui/parse-fail/bad-pkg2/root.wit:4:15 - | - 4 | use foo:bar/nonexistent.{}; - | ^---------- \ No newline at end of file +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/bad-pkg2] + +Caused by: + interface not found in package + --> tests/ui/parse-fail/bad-pkg2/root.wit:4:15 + | + 4 | use foo:bar/nonexistent.{}; + | ^---------- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-pkg3.wit.result b/crates/wit-parser/tests/ui/parse-fail/bad-pkg3.wit.result index edcb594776..f5d61a284e 100644 --- a/crates/wit-parser/tests/ui/parse-fail/bad-pkg3.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/bad-pkg3.wit.result @@ -1,5 +1,8 @@ -interface not found in package - --> tests/ui/parse-fail/bad-pkg3/root.wit:4:15 - | - 4 | use foo:bar/baz.{}; - | ^-- \ No newline at end of file +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/bad-pkg3] + +Caused by: + interface not found in package + --> tests/ui/parse-fail/bad-pkg3/root.wit:4:15 + | + 4 | use foo:bar/baz.{}; + | ^-- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-pkg4.wit.result b/crates/wit-parser/tests/ui/parse-fail/bad-pkg4.wit.result index 11c42ac812..dac51efa32 100644 --- a/crates/wit-parser/tests/ui/parse-fail/bad-pkg4.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/bad-pkg4.wit.result @@ -1,5 +1,8 @@ -type `a-name` not defined in interface - --> tests/ui/parse-fail/bad-pkg4/root.wit:3:20 - | - 3 | use foo:bar/baz.{a-name}; - | ^----- \ No newline at end of file +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/bad-pkg4] + +Caused by: + type `a-name` not defined in interface + --> tests/ui/parse-fail/bad-pkg4/root.wit:3:20 + | + 3 | use foo:bar/baz.{a-name}; + | ^----- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-pkg5.wit.result b/crates/wit-parser/tests/ui/parse-fail/bad-pkg5.wit.result index 15a04b26e2..2d37060f97 100644 --- a/crates/wit-parser/tests/ui/parse-fail/bad-pkg5.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/bad-pkg5.wit.result @@ -1,5 +1,8 @@ -type `nonexistent` not defined in interface - --> tests/ui/parse-fail/bad-pkg5/root.wit:3:20 - | - 3 | use foo:bar/baz.{nonexistent}; - | ^---------- \ No newline at end of file +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/bad-pkg5] + +Caused by: + type `nonexistent` not defined in interface + --> tests/ui/parse-fail/bad-pkg5/root.wit:3:20 + | + 3 | use foo:bar/baz.{nonexistent}; + | ^---------- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-pkg6.wit.result b/crates/wit-parser/tests/ui/parse-fail/bad-pkg6.wit.result index 40cf6058f5..c0fdc9d90e 100644 --- a/crates/wit-parser/tests/ui/parse-fail/bad-pkg6.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/bad-pkg6.wit.result @@ -1,5 +1,8 @@ -failed to find package `foo:bar` in `deps` directory - --> tests/ui/parse-fail/bad-pkg6/root.wit:3:7 - | - 3 | use foo:bar/baz.{}; - | ^------ \ No newline at end of file +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/bad-pkg6] + +Caused by: + package not found + --> tests/ui/parse-fail/bad-pkg6/root.wit:3:7 + | + 3 | use foo:bar/baz.{}; + | ^------ \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-resource15.wit.result b/crates/wit-parser/tests/ui/parse-fail/bad-resource15.wit.result index 5e840e31fc..c26467f896 100644 --- a/crates/wit-parser/tests/ui/parse-fail/bad-resource15.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/bad-resource15.wit.result @@ -1,5 +1,8 @@ -type used in a handle must be a resource - --> tests/ui/parse-fail/bad-resource15/foo.wit:6:16 - | - 6 | type t = own; - | ^ \ No newline at end of file +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/bad-resource15] + +Caused by: + type used in a handle must be a resource + --> tests/ui/parse-fail/bad-resource15/foo.wit:6:16 + | + 6 | type t = own; + | ^ \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/conflicting-package.wit.result b/crates/wit-parser/tests/ui/parse-fail/conflicting-package.wit.result index 5a6fe46efb..bdf37086eb 100644 --- a/crates/wit-parser/tests/ui/parse-fail/conflicting-package.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/conflicting-package.wit.result @@ -1,8 +1,9 @@ -failed to parse package: tests/ui/parse-fail/conflicting-package +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/conflicting-package] Caused by: - package identifier `foo:b` does not match previous package name of `foo:a` - --> tests/ui/parse-fail/conflicting-package/b.wit:1:9 - | - 1 | package foo:b; - | ^---- \ No newline at end of file + 0: failed to parse package: tests/ui/parse-fail/conflicting-package + 1: package identifier `foo:b` does not match previous package name of `foo:a` + --> tests/ui/parse-fail/conflicting-package/b.wit:1:9 + | + 1 | package foo:b; + | ^---- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/duplicate-interface2.wit.result b/crates/wit-parser/tests/ui/parse-fail/duplicate-interface2.wit.result index 7ed7ec2b29..81735c7684 100644 --- a/crates/wit-parser/tests/ui/parse-fail/duplicate-interface2.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/duplicate-interface2.wit.result @@ -1,8 +1,9 @@ -failed to parse package: tests/ui/parse-fail/duplicate-interface2 +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/duplicate-interface2] Caused by: - duplicate item named `foo` - --> tests/ui/parse-fail/duplicate-interface2/foo2.wit:3:11 - | - 3 | interface foo {} - | ^-- \ No newline at end of file + 0: failed to parse package: tests/ui/parse-fail/duplicate-interface2 + 1: duplicate item named `foo` + --> tests/ui/parse-fail/duplicate-interface2/foo2.wit:3:11 + | + 3 | interface foo {} + | ^-- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/include-foreign.wit.result b/crates/wit-parser/tests/ui/parse-fail/include-foreign.wit.result index 990f53a8ad..00257283a7 100644 --- a/crates/wit-parser/tests/ui/parse-fail/include-foreign.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/include-foreign.wit.result @@ -1,5 +1,8 @@ -world not found in package - --> tests/ui/parse-fail/include-foreign/root.wit:4:19 - | - 4 | include foo:bar/bar; - | ^-- \ No newline at end of file +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/include-foreign] + +Caused by: + world not found in package + --> tests/ui/parse-fail/include-foreign/root.wit:4:19 + | + 4 | include foo:bar/bar; + | ^-- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/multiple-package-docs.wit.result b/crates/wit-parser/tests/ui/parse-fail/multiple-package-docs.wit.result index e60c1b492c..15356332a8 100644 --- a/crates/wit-parser/tests/ui/parse-fail/multiple-package-docs.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/multiple-package-docs.wit.result @@ -1,8 +1,9 @@ -failed to parse package: tests/ui/parse-fail/multiple-package-docs +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/multiple-package-docs] Caused by: - found doc comments on multiple 'package' items - --> tests/ui/parse-fail/multiple-package-docs/b.wit:1:1 - | - 1 | /// Multiple package docs, B - | ^--------------------------- \ No newline at end of file + 0: failed to parse package: tests/ui/parse-fail/multiple-package-docs + 1: found doc comments on multiple 'package' items + --> tests/ui/parse-fail/multiple-package-docs/b.wit:1:1 + | + 1 | /// Multiple package docs, B + | ^--------------------------- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/no-access-to-sibling-use.wit.result b/crates/wit-parser/tests/ui/parse-fail/no-access-to-sibling-use.wit.result index 9693caa78b..72830f2be4 100644 --- a/crates/wit-parser/tests/ui/parse-fail/no-access-to-sibling-use.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/no-access-to-sibling-use.wit.result @@ -1,8 +1,9 @@ -failed to parse package: tests/ui/parse-fail/no-access-to-sibling-use +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/no-access-to-sibling-use] Caused by: - interface or world `bar-renamed` does not exist - --> tests/ui/parse-fail/no-access-to-sibling-use/foo.wit:3:5 - | - 3 | use bar-renamed; - | ^---------- \ No newline at end of file + 0: failed to parse package: tests/ui/parse-fail/no-access-to-sibling-use + 1: interface or world `bar-renamed` does not exist + --> tests/ui/parse-fail/no-access-to-sibling-use/foo.wit:3:5 + | + 3 | use bar-renamed; + | ^---------- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/non-existance-world-include.wit.result b/crates/wit-parser/tests/ui/parse-fail/non-existance-world-include.wit.result index ed8f228b25..13a112277d 100644 --- a/crates/wit-parser/tests/ui/parse-fail/non-existance-world-include.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/non-existance-world-include.wit.result @@ -1,5 +1,8 @@ -world not found in package - --> tests/ui/parse-fail/non-existance-world-include/root.wit:4:19 - | - 4 | include foo:baz/non-existance; - | ^------------ \ No newline at end of file +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/non-existance-world-include] + +Caused by: + world not found in package + --> tests/ui/parse-fail/non-existance-world-include/root.wit:4:19 + | + 4 | include foo:baz/non-existance; + | ^------------ \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/pkg-cycle.wit.result b/crates/wit-parser/tests/ui/parse-fail/pkg-cycle.wit.result index d87b514871..f64970931d 100644 --- a/crates/wit-parser/tests/ui/parse-fail/pkg-cycle.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/pkg-cycle.wit.result @@ -1,5 +1,8 @@ -package depends on itself - --> tests/ui/parse-fail/pkg-cycle/deps/a1/root.wit:3:7 - | - 3 | use foo:a1/foo.{}; - | ^----- \ No newline at end of file +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/pkg-cycle] + +Caused by: + package depends on itself + --> tests/ui/parse-fail/pkg-cycle/deps/a1/root.wit:3:7 + | + 3 | use foo:a1/foo.{}; + | ^----- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/pkg-cycle2.wit.result b/crates/wit-parser/tests/ui/parse-fail/pkg-cycle2.wit.result index e45eec8350..53054871c2 100644 --- a/crates/wit-parser/tests/ui/parse-fail/pkg-cycle2.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/pkg-cycle2.wit.result @@ -1,5 +1,8 @@ -package depends on itself - --> tests/ui/parse-fail/pkg-cycle2/deps/a1/root.wit:3:7 - | - 3 | use foo:a2/foo.{}; - | ^----- \ No newline at end of file +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/pkg-cycle2] + +Caused by: + package depends on itself + --> tests/ui/parse-fail/pkg-cycle2/deps/a1/root.wit:3:7 + | + 3 | use foo:a2/foo.{}; + | ^----- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/type-and-resource-same-name.wit.result b/crates/wit-parser/tests/ui/parse-fail/type-and-resource-same-name.wit.result index 355be9b297..d86ebfb39c 100644 --- a/crates/wit-parser/tests/ui/parse-fail/type-and-resource-same-name.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/type-and-resource-same-name.wit.result @@ -1,5 +1,8 @@ -type used in a handle must be a resource - --> tests/ui/parse-fail/type-and-resource-same-name/foo.wit:7:20 - | - 7 | type t2 = borrow; - | ^ \ No newline at end of file +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/type-and-resource-same-name] + +Caused by: + type used in a handle must be a resource + --> tests/ui/parse-fail/type-and-resource-same-name/foo.wit:7:20 + | + 7 | type t2 = borrow; + | ^ \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/unresolved-use10.wit.result b/crates/wit-parser/tests/ui/parse-fail/unresolved-use10.wit.result index 1ad83de299..75343de663 100644 --- a/crates/wit-parser/tests/ui/parse-fail/unresolved-use10.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/unresolved-use10.wit.result @@ -1,8 +1,9 @@ -failed to parse package: tests/ui/parse-fail/unresolved-use10 +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/unresolved-use10] Caused by: - name `thing` is not defined - --> tests/ui/parse-fail/unresolved-use10/bar.wit:4:12 - | - 4 | use foo.{thing}; - | ^---- \ No newline at end of file + 0: failed to parse package: tests/ui/parse-fail/unresolved-use10 + 1: name `thing` is not defined + --> tests/ui/parse-fail/unresolved-use10/bar.wit:4:12 + | + 4 | use foo.{thing}; + | ^---- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/use-and-include-world.wit.result b/crates/wit-parser/tests/ui/parse-fail/use-and-include-world.wit.result index f278affe72..0d4fcd91ff 100644 --- a/crates/wit-parser/tests/ui/parse-fail/use-and-include-world.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/use-and-include-world.wit.result @@ -1,8 +1,9 @@ -failed to parse package: tests/ui/parse-fail/use-and-include-world +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/use-and-include-world] Caused by: - name `bar` is defined as an interface, not a world - --> tests/ui/parse-fail/use-and-include-world/root.wit:6:21 - | - 6 | include foo:baz/bar; - | ^-- \ No newline at end of file + 0: failed to parse package: tests/ui/parse-fail/use-and-include-world + 1: name `bar` is defined as an interface, not a world + --> tests/ui/parse-fail/use-and-include-world/root.wit:6:21 + | + 6 | include foo:baz/bar; + | ^-- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/use-world.wit.result b/crates/wit-parser/tests/ui/parse-fail/use-world.wit.result index d21d706277..df56488b01 100644 --- a/crates/wit-parser/tests/ui/parse-fail/use-world.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/use-world.wit.result @@ -1,5 +1,8 @@ -interface not found in package - --> tests/ui/parse-fail/use-world/root.wit:3:13 - | - 3 | use foo:baz/bar; - | ^-- \ No newline at end of file +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/use-world] + +Caused by: + interface not found in package + --> tests/ui/parse-fail/use-world/root.wit:3:13 + | + 3 | use foo:baz/bar; + | ^-- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/simple-wasm-text.wat b/crates/wit-parser/tests/ui/simple-wasm-text.wat new file mode 100644 index 0000000000..71bd27c1e5 --- /dev/null +++ b/crates/wit-parser/tests/ui/simple-wasm-text.wat @@ -0,0 +1,6 @@ +(component + (type $x (component + (export "a:b/x" (instance)) + )) + (export "x" (type $x)) +) diff --git a/crates/wit-parser/tests/ui/simple-wasm-text.wit.json b/crates/wit-parser/tests/ui/simple-wasm-text.wit.json new file mode 100644 index 0000000000..969c13a506 --- /dev/null +++ b/crates/wit-parser/tests/ui/simple-wasm-text.wit.json @@ -0,0 +1,21 @@ +{ + "worlds": [], + "interfaces": [ + { + "name": "x", + "types": {}, + "functions": {}, + "package": 0 + } + ], + "types": [], + "packages": [ + { + "name": "a:b", + "interfaces": { + "x": 0 + }, + "worlds": {} + } + ] +} \ No newline at end of file diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs index 173d317635..bd7218a38b 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -8,9 +8,9 @@ use anyhow::{bail, Context, Result}; use clap::Parser; use wasm_tools::Output; +use wat::Detect; use wit_component::{ - embed_component_metadata, is_wasm_binary_or_wat, parse_wit_from_path, ComponentEncoder, - DecodedWasm, Linker, StringEncoding, WitPrinter, + embed_component_metadata, ComponentEncoder, DecodedWasm, Linker, StringEncoding, WitPrinter, }; use wit_parser::{Resolve, UnresolvedPackage}; @@ -241,7 +241,8 @@ impl EmbedOpts { } else { Some(self.io.parse_input_wasm()?) }; - let (resolve, id) = parse_wit_from_path(self.wit)?; + let mut resolve = Resolve::default(); + let id = resolve.push_path(&self.wit)?.0; let world = resolve.select_world(id, self.world.as_deref())?; let mut wasm = wasm.unwrap_or_else(|| wit_component::dummy_module(&resolve, world)); @@ -484,7 +485,8 @@ impl WitOpts { // `parse_wit_from_path`. if let Some(input) = &self.input { if input.is_dir() { - let (resolve, id) = parse_wit_from_path(input)?; + let mut resolve = Resolve::default(); + let id = resolve.push_dir(&input)?.0; return Ok(DecodedWasm::WitPackage(resolve, id)); } } @@ -508,31 +510,34 @@ impl WitOpts { } }; - if is_wasm_binary_or_wat(&input) { - // Use `wat` to possible translate the text format, and then - // afterwards use either `decode` or `metadata::decode` depending on - // if the input is a component or a core wasm mdoule. - let input = wat::parse_bytes(&input).map_err(|mut e| { - e.set_path(path); - e - })?; - if wasmparser::Parser::is_component(&input) { - wit_component::decode(&input) - } else { - let (_wasm, bindgen) = wit_component::metadata::decode(&input)?; - Ok(DecodedWasm::Component(bindgen.resolve, bindgen.world)) + match Detect::from_bytes(&input) { + Detect::WasmBinary | Detect::WasmText => { + // Use `wat` to possible translate the text format, and then + // afterwards use either `decode` or `metadata::decode` depending on + // if the input is a component or a core wasm module. + let input = wat::parse_bytes(&input).map_err(|mut e| { + e.set_path(path); + e + })?; + if wasmparser::Parser::is_component(&input) { + wit_component::decode(&input) + } else { + let (_wasm, bindgen) = wit_component::metadata::decode(&input)?; + Ok(DecodedWasm::Component(bindgen.resolve, bindgen.world)) + } + } + Detect::Unknown => { + // This is a single WIT file, so create the single-file package and + // return it. + let input = match std::str::from_utf8(&input) { + Ok(s) => s, + Err(_) => bail!("input was not valid utf-8"), + }; + let mut resolve = Resolve::default(); + let pkg = UnresolvedPackage::parse(path, input)?; + let id = resolve.push(pkg)?; + Ok(DecodedWasm::WitPackage(resolve, id)) } - } else { - // This is a single WIT file, so create the single-file package and - // return it. - let input = match std::str::from_utf8(&input) { - Ok(s) => s, - Err(_) => bail!("input was not valid utf-8"), - }; - let mut resolve = Resolve::default(); - let pkg = UnresolvedPackage::parse(path, input)?; - let id = resolve.push(pkg)?; - Ok(DecodedWasm::WitPackage(resolve, id)) } } @@ -655,7 +660,8 @@ impl TargetsOpts { /// Executes the application. fn run(self) -> Result<()> { - let (resolve, package_id) = parse_wit_from_path(&self.wit)?; + let mut resolve = Resolve::default(); + let package_id = resolve.push_path(&self.wit)?.0; let world = resolve.select_world(package_id, self.world.as_deref())?; let component_to_test = self.input.parse_wasm()?; @@ -699,7 +705,8 @@ impl SemverCheckOpts { } fn run(self) -> Result<()> { - let (resolve, package_id) = parse_wit_from_path(&self.wit)?; + let mut resolve = Resolve::default(); + let package_id = resolve.push_path(&self.wit)?.0; let prev = resolve.select_world(package_id, Some(&self.prev))?; let new = resolve.select_world(package_id, Some(&self.new))?; wit_component::semver_check(resolve, prev, new)?;