From a706d3600885933ece7f3b84f861b75868011b97 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sat, 15 Aug 2026 01:10:17 -0400 Subject: [PATCH 01/12] Add support for package-scope WIT types Signed-off-by: Yordis Prieto --- crates/wit-component/src/encoding.rs | 2 +- crates/wit-component/src/encoding/wit.rs | 54 +++ crates/wit-component/src/lib.rs | 193 +++++++- crates/wit-component/src/metadata.rs | 1 + crates/wit-component/src/printing.rs | 149 +++++- .../tests/interfaces/package-scope-docs.wat | 41 ++ .../tests/interfaces/package-scope-docs.wit | 16 + .../interfaces/package-scope-docs.wit.print | 16 + .../interfaces/package-scope-field-docs.wat | 63 +++ .../interfaces/package-scope-field-docs.wit | 38 ++ .../package-scope-field-docs.wit.print | 39 ++ .../interfaces/package-scope-foreign-use.wat | 51 ++ .../package-scope-foreign-use/consumer.wit | 16 + .../consumer.wit.print | 20 + .../deps/types/types.wit | 10 + .../tests/interfaces/package-scope-nested.wat | 41 ++ .../tests/interfaces/package-scope-nested.wit | 14 + .../interfaces/package-scope-nested.wit.print | 14 + .../tests/interfaces/package-scope-types.wat | 86 ++++ .../tests/interfaces/package-scope-types.wit | 39 ++ .../interfaces/package-scope-types.wit.print | 43 ++ .../interfaces/package-scope-world-only.wat | 26 ++ .../interfaces/package-scope-world-only.wit | 12 + .../package-scope-world-only.wit.print | 13 + crates/wit-dylib/tests/roundtrip.rs | 1 + crates/wit-encoder/src/from_parser.rs | 178 ++++++- crates/wit-encoder/src/package.rs | 58 ++- .../wit-encoder/tests/package-scope-types.rs | 314 +++++++++++++ crates/wit-parser/src/ast.rs | 32 +- crates/wit-parser/src/ast/resolve.rs | 437 ++++++++++++++++-- crates/wit-parser/src/decoding.rs | 343 ++++++++++++-- crates/wit-parser/src/lib.rs | 18 + crates/wit-parser/src/metadata.rs | 18 + crates/wit-parser/src/resolve/clone.rs | 12 + crates/wit-parser/src/resolve/mod.rs | 227 +++++++-- .../wit-parser/tests/package-scope-merge.rs | 227 +++++++++ crates/wit-parser/tests/ui/async.wit.json | 3 +- crates/wit-parser/tests/ui/comments.wit.json | 3 +- .../tests/ui/complex-include.wit.json | 9 +- .../tests/ui/cross-package-resource.wit.json | 6 +- crates/wit-parser/tests/ui/diamond1.wit.json | 9 +- .../tests/ui/disambiguate-diamond.wit.json | 3 +- crates/wit-parser/tests/ui/empty.wit.json | 3 +- .../tests/ui/error-context.wit.json | 3 +- .../tests/ui/feature-gates.wit.json | 3 +- .../tests/ui/feature-types.wit.json | 3 +- .../tests/ui/foreign-deps-union.wit.json | 21 +- .../wit-parser/tests/ui/foreign-deps.wit.json | 21 +- .../ui/foreign-interface-dep-gated.wit.json | 9 +- .../tests/ui/foreign-world-dep-gated.wit.json | 9 +- crates/wit-parser/tests/ui/functions.wit.json | 3 +- .../tests/ui/gated-include.wit.json | 3 +- crates/wit-parser/tests/ui/gated-use.wit.json | 6 +- .../tests/ui/ignore-files-deps.wit.json | 6 +- .../tests/ui/import-export-overlap1.wit.json | 3 +- .../tests/ui/import-export-overlap2.wit.json | 3 +- .../wit-parser/tests/ui/include-reps.wit.json | 3 +- .../tests/ui/kebab-name-include-with.wit.json | 3 +- .../tests/ui/kinds-of-deps.wit.json | 15 +- .../wit-parser/tests/ui/many-names.wit.json | 3 +- crates/wit-parser/tests/ui/maps.wit.json | 3 +- .../ui/multi-file-multi-package.wit.json | 15 +- .../wit-parser/tests/ui/multi-file.wit.json | 3 +- .../tests/ui/multi-package-deps.wit.json | 12 +- .../ui/multi-package-gated-include.wit.json | 21 +- .../ui/multi-package-shared-deps.wit.json | 15 +- .../ui/multi-package-transitive-deps.wit.json | 18 +- .../ui/name-both-resource-and-type.wit.json | 6 +- .../tests/ui/package-scope-alias-chain.wit | 13 + .../ui/package-scope-alias-chain.wit.json | 76 +++ .../tests/ui/package-scope-all-gated.wit | 19 + .../tests/ui/package-scope-all-gated.wit.json | 43 ++ .../tests/ui/package-scope-deprecated.wit | 15 + .../ui/package-scope-deprecated.wit.json | 73 +++ .../ui/package-scope-foreign-as.wit.json | 85 ++++ .../package-scope-foreign-as/deps/types.wit | 9 + .../ui/package-scope-foreign-as/root.wit | 11 + .../ui/package-scope-foreign-compose.wit.json | 103 +++++ .../deps/types.wit | 9 + .../ui/package-scope-foreign-compose/root.wit | 15 + .../ui/package-scope-foreign-iface.wit.json | 77 +++ .../deps/types.wit | 10 + .../ui/package-scope-foreign-iface/root.wit | 7 + .../ui/package-scope-foreign-use.wit.json | 85 ++++ .../package-scope-foreign-use/deps/types.wit | 9 + .../ui/package-scope-foreign-use/root.wit | 11 + .../ui/package-scope-func-name-overlap.wit | 16 + .../package-scope-func-name-overlap.wit.json | 68 +++ .../tests/ui/package-scope-gated.wit | 25 + .../tests/ui/package-scope-gated.wit.json | 103 +++++ .../tests/ui/package-scope-include-with.wit | 15 + .../ui/package-scope-include-with.wit.json | 94 ++++ .../tests/ui/package-scope-include.wit | 15 + .../tests/ui/package-scope-include.wit.json | 91 ++++ .../tests/ui/package-scope-local-use.wit | 16 + .../tests/ui/package-scope-local-use.wit.json | 71 +++ .../ui/package-scope-multifile-world.wit.json | 74 +++ .../package-scope-multifile-world/types.wit | 6 + .../package-scope-multifile-world/world.wit | 6 + .../tests/ui/package-scope-multifile.wit.json | 71 +++ .../tests/ui/package-scope-multifile/api.wit | 9 + .../ui/package-scope-multifile/types.wit | 6 + .../tests/ui/package-scope-nested-use.wit | 23 + .../ui/package-scope-nested-use.wit.json | 91 ++++ .../tests/ui/package-scope-nested.wit | 16 + .../tests/ui/package-scope-nested.wit.json | 77 +++ .../tests/ui/package-scope-rich-aliases.wit | 16 + .../ui/package-scope-rich-aliases.wit.json | 132 ++++++ .../tests/ui/package-scope-self-use.wit | 16 + .../tests/ui/package-scope-self-use.wit.json | 71 +++ .../tests/ui/package-scope-shadow.wit | 15 + .../tests/ui/package-scope-shadow.wit.json | 82 ++++ .../tests/ui/package-scope-since.wit | 20 + .../tests/ui/package-scope-since.wit.json | 104 +++++ .../tests/ui/package-scope-types.wit | 39 ++ .../tests/ui/package-scope-types.wit.json | 195 ++++++++ .../ui/package-scope-versioned-use.wit.json | 85 ++++ .../deps/types.wit | 9 + .../ui/package-scope-versioned-use/root.wit | 11 + .../tests/ui/package-scope-world.wit | 12 + .../tests/ui/package-scope-world.wit.json | 86 ++++ .../tests/ui/package-syntax1.wit.json | 3 +- .../tests/ui/package-syntax3.wit.json | 3 +- .../tests/ui/package-syntax4.wit.json | 3 +- .../ui/packages-multiple-nested.wit.json | 9 +- ...kages-nested-colliding-decl-names.wit.json | 9 +- ...ckages-nested-internal-references.wit.json | 9 +- .../ui/packages-nested-with-semver.wit.json | 9 +- .../tests/ui/packages-single-nested.wit.json | 6 +- .../no-access-to-sibling-use.wit.result | 2 +- .../package-scope-dup-multifile.wit.result | 5 + .../package-scope-dup-multifile/types1.wit | 6 + .../package-scope-dup-multifile/types2.wit | 6 + .../ui/parse-fail/package-scope-gated-ref.wit | 10 + .../package-scope-gated-ref.wit.result | 5 + ...ackage-scope-multifile-ns-clash.wit.result | 5 + .../package-scope-multifile-ns-clash/api.wit | 5 + .../types.wit | 6 + .../package-scope-nested-outer-leak.wit | 17 + ...package-scope-nested-outer-leak.wit.result | 5 + .../ui/parse-fail/package-scope-resource.wit | 4 + .../package-scope-resource.wit.result | 5 + .../parse-fail/package-scope-self-use-dup.wit | 14 + .../package-scope-self-use-dup.wit.result | 5 + .../ui/parse-fail/package-scope-since-ref.wit | 10 + .../package-scope-since-ref.wit.result | 5 + .../parse-fail/package-scope-type-cycle.wit | 4 + .../package-scope-type-cycle.wit.result | 5 + .../package-scope-type-iface-clash.wit | 10 + .../package-scope-type-iface-clash.wit.result | 5 + .../package-scope-type-world-clash.wit | 10 + .../package-scope-type-world-clash.wit.result | 5 + .../package-scope-use-cross-file.wit.result | 5 + .../deps/foreign.wit | 9 + .../package-scope-use-cross-file/types.wit | 11 + .../package-scope-use-cross-file/uses.wit | 3 + .../package-scope-use-missing.wit.result | 5 + .../package-scope-use-missing/deps/types.wit | 5 + .../package-scope-use-missing/root.wit | 7 + ...scope-use-type-as-iface-foreign.wit.result | 5 + .../deps/types.wit | 9 + .../root.wit | 6 + .../package-scope-use-type-as-iface.wit | 10 + ...package-scope-use-type-as-iface.wit.result | 5 + .../package-scope-use-world.wit.result | 5 + .../package-scope-use-world/deps/types.wit | 5 + .../package-scope-use-world/root.wit | 7 + .../unresolved-interface3.wit.result | 2 +- .../tests/ui/parse-fail/use-world.wit.result | 2 +- crates/wit-parser/tests/ui/random.wit.json | 3 +- .../tests/ui/resources-empty.wit.json | 3 +- .../resources-multiple-returns-own.wit.json | 3 +- .../tests/ui/resources-multiple.wit.json | 3 +- .../tests/ui/resources-return-own.wit.json | 3 +- crates/wit-parser/tests/ui/resources.wit.json | 3 +- .../wit-parser/tests/ui/resources1.wit.json | 3 +- .../tests/ui/same-name-import-export.wit.json | 3 +- .../wit-parser/tests/ui/shared-types.wit.json | 3 +- .../tests/ui/simple-wasm-text.wit.json | 3 +- .../tests/ui/since-and-unstable.wit.json | 3 +- .../tests/ui/streams-and-futures.wit.json | 3 +- .../tests/ui/stress-export-elaborate.wit.json | 3 +- .../tests/ui/type-then-eof.wit.json | 3 +- crates/wit-parser/tests/ui/types.wit.json | 3 +- .../wit-parser/tests/ui/union-fuzz-1.wit.json | 3 +- .../wit-parser/tests/ui/union-fuzz-2.wit.json | 3 +- .../tests/ui/unstable-resource.wit.json | 6 +- crates/wit-parser/tests/ui/use-chain.wit.json | 3 +- crates/wit-parser/tests/ui/use.wit.json | 3 +- .../tests/ui/version-syntax.wit.json | 30 +- crates/wit-parser/tests/ui/versions.wit.json | 9 +- crates/wit-parser/tests/ui/wasi.wit.json | 3 +- .../tests/ui/with-resource-as.wit.json | 3 +- .../tests/ui/world-diamond.wit.json | 3 +- .../tests/ui/world-iface-no-collide.wit.json | 3 +- .../tests/ui/world-implicit-import1.wit.json | 3 +- .../tests/ui/world-implicit-import2.wit.json | 3 +- .../tests/ui/world-implicit-import3.wit.json | 3 +- .../tests/ui/world-same-fields4.wit.json | 3 +- .../tests/ui/world-top-level-funcs.wit.json | 3 +- .../ui/world-top-level-resources.wit.json | 3 +- .../tests/ui/worlds-union-dedup.wit.json | 3 +- .../tests/ui/worlds-with-types.wit.json | 3 +- 203 files changed, 5708 insertions(+), 300 deletions(-) create mode 100644 crates/wit-component/tests/interfaces/package-scope-docs.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-docs.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-docs.wit.print create mode 100644 crates/wit-component/tests/interfaces/package-scope-field-docs.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-field-docs.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-field-docs.wit.print create mode 100644 crates/wit-component/tests/interfaces/package-scope-foreign-use.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit.print create mode 100644 crates/wit-component/tests/interfaces/package-scope-foreign-use/deps/types/types.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-nested.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-nested.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-nested.wit.print create mode 100644 crates/wit-component/tests/interfaces/package-scope-types.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-types.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-types.wit.print create mode 100644 crates/wit-component/tests/interfaces/package-scope-world-only.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-world-only.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-world-only.wit.print create mode 100644 crates/wit-encoder/tests/package-scope-types.rs create mode 100644 crates/wit-parser/tests/package-scope-merge.rs create mode 100644 crates/wit-parser/tests/ui/package-scope-alias-chain.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-alias-chain.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-all-gated.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-all-gated.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-deprecated.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-deprecated.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-foreign-as.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-foreign-as/deps/types.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-foreign-as/root.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-foreign-compose.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-foreign-compose/deps/types.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-foreign-compose/root.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-foreign-iface.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-foreign-iface/deps/types.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-foreign-iface/root.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-foreign-use.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-foreign-use/deps/types.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-foreign-use/root.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-func-name-overlap.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-func-name-overlap.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-gated.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-gated.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-include-with.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-include-with.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-include.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-include.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-local-use.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-local-use.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-multifile-world.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-multifile-world/types.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-multifile-world/world.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-multifile.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-multifile/api.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-multifile/types.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-nested-use.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-nested-use.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-nested.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-nested.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-rich-aliases.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-rich-aliases.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-self-use.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-self-use.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-shadow.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-shadow.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-since.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-since.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-types.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-types.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-versioned-use.wit.json create mode 100644 crates/wit-parser/tests/ui/package-scope-versioned-use/deps/types.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-versioned-use/root.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-world.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-world.wit.json create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-dup-multifile.wit.result create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-dup-multifile/types1.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-dup-multifile/types2.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-gated-ref.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-gated-ref.wit.result create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-multifile-ns-clash.wit.result create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-multifile-ns-clash/api.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-multifile-ns-clash/types.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-nested-outer-leak.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-nested-outer-leak.wit.result create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-resource.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-resource.wit.result create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-self-use-dup.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-self-use-dup.wit.result create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-since-ref.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-since-ref.wit.result create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-type-cycle.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-type-cycle.wit.result create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-type-iface-clash.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-type-iface-clash.wit.result create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-type-world-clash.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-type-world-clash.wit.result create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-use-cross-file.wit.result create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-use-cross-file/deps/foreign.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-use-cross-file/types.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-use-cross-file/uses.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-use-missing.wit.result create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-use-missing/deps/types.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-use-missing/root.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface-foreign.wit.result create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface-foreign/deps/types.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface-foreign/root.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface.wit.result create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-use-world.wit.result create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-use-world/deps/types.wit create mode 100644 crates/wit-parser/tests/ui/parse-fail/package-scope-use-world/root.wit diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index b47fba2fb8..e4e141345e 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -713,7 +713,7 @@ impl<'a> EncodingState<'a> { self.component .alias_export(instance, name, ComponentExportKind::Type) } - TypeOwner::None => panic!("resources must have an owner"), + TypeOwner::Package(_) | TypeOwner::None => panic!("resources must have an owner"), } } diff --git a/crates/wit-component/src/encoding/wit.rs b/crates/wit-component/src/encoding/wit.rs index a70f90cc38..9a4a84ef5f 100644 --- a/crates/wit-component/src/encoding/wit.rs +++ b/crates/wit-component/src/encoding/wit.rs @@ -82,7 +82,12 @@ pub fn encode_world(resolve: &Resolve, world_id: WorldId) -> Result { component.interface = None; + // Package-scope types referenced by freestanding world funcs + // must be imported into the world component type (same as + // world-owned named types), not exported. + component.import_types = true; let idx = component.encode_func_type(resolve, f)?; + component.import_types = false; ComponentTypeRef::Func(idx) } WorldItem::Type { id, .. } => { @@ -108,7 +113,12 @@ pub fn encode_world(resolve: &Resolve, world_id: WorldId) -> Result { component.interface = None; + // Package-scope types referenced by freestanding world funcs + // must be imported into the world component type (same as + // world-owned named types), not exported. + component.import_types = true; let idx = component.encode_func_type(resolve, f)?; + component.import_types = false; ComponentTypeRef::Func(idx) } WorldItem::Type { .. } => unreachable!(), @@ -142,6 +152,18 @@ struct Encoder<'a> { impl Encoder<'_> { fn run(&mut self) -> Result<()> { + // Encode package-scope types first so V2 sniffing still sees a Label + // as the first export name when types are present. + { + let mut encoder = PackageTypeEncoder { + component: &mut self.component, + type_encoding_maps: Default::default(), + }; + for (_, &id) in self.resolve.packages[self.package].types.iter() { + encoder.encode_valtype(self.resolve, &Type::Id(id))?; + } + } + // Encode all interfaces as component types and then export them. for (name, &id) in self.resolve.packages[self.package].interfaces.iter() { let component_ty = self.encode_interface(id)?; @@ -406,3 +428,35 @@ impl<'a> ValtypeEncoder<'a> for InterfaceEncoder<'a> { } } } + +struct PackageTypeEncoder<'a> { + component: &'a mut ComponentBuilder, + type_encoding_maps: TypeEncodingMaps<'a>, +} + +impl<'a> ValtypeEncoder<'a> for PackageTypeEncoder<'a> { + fn defined_type(&mut self) -> (u32, ComponentDefinedTypeEncoder<'_>) { + self.component.type_defined(None) + } + fn define_function_type(&mut self) -> (u32, ComponentFuncTypeEncoder<'_>) { + self.component.type_function(None) + } + fn export_type(&mut self, index: u32, name: ComponentExternName<'a>) -> Option { + Some( + self.component + .export(name, ComponentExportKind::Type, index, None), + ) + } + fn export_resource(&mut self, _name: ComponentExternName<'a>) -> u32 { + unreachable!("package-scope resources are not allowed") + } + fn type_encoding_maps(&mut self) -> &mut TypeEncodingMaps<'a> { + &mut self.type_encoding_maps + } + fn interface(&self) -> Option { + None + } + fn import_type(&mut self, _owner: InterfaceId, _id: TypeId) -> u32 { + unreachable!("package-scope types do not import from interfaces") + } +} diff --git a/crates/wit-component/src/lib.rs b/crates/wit-component/src/lib.rs index 11f57201e5..1f7e80b10b 100644 --- a/crates/wit-component/src/lib.rs +++ b/crates/wit-component/src/lib.rs @@ -111,10 +111,10 @@ pub fn embed_component_metadata( #[cfg(test)] mod tests { use anyhow::Result; - use wasmparser::Payload; + use wasmparser::{Payload, WasmFeatures}; use wit_parser::Resolve; - use super::{StringEncoding, embed_component_metadata}; + use super::{StringEncoding, embed_component_metadata, encode}; const MODULE_WAT: &str = r#" (module @@ -175,4 +175,193 @@ world test-world {} Ok(()) } + + #[test] + fn package_scope_foreign_type_encodes_without_import() -> Result<()> { + let mut resolve = Resolve::new(); + resolve.push_str( + "types.wit", + r#" +package local:types; + +record point { + x: u32, + y: u32, +} + +interface unused {} +"#, + )?; + let pkg = resolve.push_str( + "consumer.wit", + r#" +package local:consumer; + +use local:types/point; + +interface api { + move-to: func(p: point); +} + +world w { + export api; +} +"#, + )?; + + let wasm = encode(&resolve, pkg)?; + let wat = wasmprinter::print_bytes(&wasm)?; + assert!( + !wat.contains("(import"), + "package-scope foreign type must be inlined, not imported:\n{wat}" + ); + assert!( + wat.contains("(record"), + "expected inlined record for foreign package type:\n{wat}" + ); + wasmparser::Validator::new_with_features(WasmFeatures::all()).validate_all(&wasm)?; + + // Text print of the resolved package should emit a toplevel use. + let mut printer = crate::WitPrinter::default(); + printer.print(&resolve, pkg, &[])?; + let printed = printer.output.to_string(); + assert!( + printed.contains("use local:types/point;"), + "expected toplevel use printback:\n{printed}" + ); + + Ok(()) + } + + #[test] + fn package_scope_foreign_type_as_printback() -> Result<()> { + let mut resolve = Resolve::new(); + resolve.push_str( + "types.wit", + r#" +package local:types; + +record point { + x: u32, + y: u32, +} + +interface unused {} +"#, + )?; + let pkg = resolve.push_str( + "consumer.wit", + r#" +package local:consumer; + +use local:types/point as pt; + +interface api { + move-to: func(p: pt); +} + +world w { + export api; +} +"#, + )?; + + let mut printer = crate::WitPrinter::default(); + printer.print(&resolve, pkg, &[])?; + let printed = printer.output.to_string(); + // Printer canonicalizes to the original package type name rather than + // preserving the local `as` alias. + assert!( + printed.contains("use local:types/point"), + "expected toplevel use printback:\n{printed}" + ); + assert!( + printed.contains("func(p: point)") || printed.contains("func(p: pt)"), + "expected func to reference the foreign type:\n{printed}" + ); + + let wasm = encode(&resolve, pkg)?; + wasmparser::Validator::new_with_features(WasmFeatures::all()).validate_all(&wasm)?; + Ok(()) + } + + #[test] + fn package_scope_nested_package_encodes() -> Result<()> { + let mut resolve = Resolve::new(); + resolve.push_str( + "nested.wit", + r#" +package local:root; + +package local:nested { + record point { + x: u32, + y: u32, + } + + interface api { + move-to: func(p: point); + } + + world w { + export api; + } +} +"#, + )?; + let nested = *resolve + .package_names + .iter() + .find(|(name, _)| name.name == "nested") + .map(|(_, id)| id) + .expect("nested package"); + + let wasm = encode(&resolve, nested)?; + let wat = wasmprinter::print_bytes(&wasm)?; + assert!( + wat.contains("(export (;1;) \"point\" (type 0))"), + "expected package-scope point export:\n{wat}" + ); + assert!(!wat.contains("(import"), "{wat}"); + wasmparser::Validator::new_with_features(WasmFeatures::all()).validate_all(&wasm)?; + + let decoded = crate::decode(&wasm)?; + let mut printer = crate::WitPrinter::default(); + printer.print(decoded.resolve(), decoded.package(), &[])?; + let printed = printer.output.to_string(); + assert!(printed.contains("record point"), "{printed}"); + assert!(printed.contains("interface api"), "{printed}"); + + Ok(()) + } + + #[test] + fn package_scope_type_only_package_decode_fails() -> Result<()> { + let mut resolve = Resolve::new(); + let pkg = resolve.push_str( + "types-only.wit", + r#" +package local:types; + +record point { + x: u32, + y: u32, +} +"#, + )?; + + let wasm = encode(&resolve, pkg)?; + match crate::decode(&wasm) { + Ok(_) => panic!("type-only packages cannot round-trip"), + Err(err) => { + let msg = format!("{err:#}"); + assert!( + msg.contains("only package-scope types") + || msg.contains("no interfaces or worlds"), + "unexpected error: {msg}" + ); + } + } + Ok(()) + } } diff --git a/crates/wit-component/src/metadata.rs b/crates/wit-component/src/metadata.rs index 361facbc78..0e933deb29 100644 --- a/crates/wit-component/src/metadata.rs +++ b/crates/wit-component/src/metadata.rs @@ -82,6 +82,7 @@ impl Default for Bindgen { docs: Default::default(), interfaces: Default::default(), worlds: Default::default(), + types: Default::default(), }); let world = resolve.worlds.alloc(World { name: "root".to_string(), diff --git a/crates/wit-component/src/printing.rs b/crates/wit-component/src/printing.rs index a9381465a9..64c7e97732 100644 --- a/crates/wit-component/src/printing.rs +++ b/crates/wit-component/src/printing.rs @@ -1,6 +1,6 @@ use anyhow::{Result, anyhow, bail}; use std::borrow::Cow; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::fmt::Display; use std::mem; use std::ops::Deref; @@ -68,8 +68,8 @@ impl WitPrinter { pkg: PackageId, is_main: bool, ) -> Result<()> { - let pkg = &resolve.packages[pkg]; - self.print_package_outer(pkg)?; + let package = &resolve.packages[pkg]; + self.print_package_outer(package)?; if is_main { self.output.semicolon(); @@ -78,7 +78,21 @@ impl WitPrinter { self.output.indent_start(); } - for (name, id) in pkg.interfaces.iter() { + for (name, id) in package.types.iter() { + let ty = &resolve.types[*id]; + self.print_docs(&ty.docs); + self.print_stability(&ty.stability); + self.print_external_id(ty.external_id.as_deref()); + self.declare_type(resolve, &Type::Id(*id))?; + let _ = name; + if is_main { + self.output.newline(); + } + } + + self.print_foreign_package_type_uses(resolve, pkg)?; + + for (name, id) in package.interfaces.iter() { self.print_interface_outer(resolve, *id, name)?; self.output.indent_start(); self.print_interface(resolve, *id)?; @@ -88,7 +102,7 @@ impl WitPrinter { } } - for (name, id) in pkg.worlds.iter() { + for (name, id) in package.worlds.iter() { self.print_docs(&resolve.worlds[*id].docs); self.print_stability(&resolve.worlds[*id].stability); self.output.keyword("world"); @@ -104,6 +118,54 @@ impl WitPrinter { Ok(()) } + /// Emit toplevel `use ns:pkg/name;` for foreign package-scope types that + /// are referenced from this package's interfaces/worlds. + fn print_foreign_package_type_uses( + &mut self, + resolve: &Resolve, + pkg: PackageId, + ) -> Result<()> { + let mut live = LiveTypes::default(); + for (_, &id) in resolve.packages[pkg].interfaces.iter() { + live.add_interface(resolve, id); + } + for (_, &id) in resolve.packages[pkg].worlds.iter() { + live.add_world(resolve, id); + } + + // Sort for stable output: (package name string, type name). + let mut uses = BTreeMap::new(); + for id in live.iter() { + let ty = &resolve.types[id]; + let TypeOwner::Package(owner) = ty.owner else { + continue; + }; + if owner == pkg { + continue; + } + let Some(name) = ty.name.as_deref() else { + continue; + }; + uses.insert( + ( + resolve.packages[owner].name.to_string(), + name.to_string(), + owner, + ), + (), + ); + } + + for ((_, name, owner), _) in uses { + self.output.keyword("use"); + self.output.str(" "); + self.print_path_to_package_type(resolve, owner, &name, pkg)?; + self.output.semicolon(); + self.output.newline(); + } + Ok(()) + } + /// Print the specified package without its content. /// Does not print the semicolon nor starts the indentation. pub fn print_package_outer(&mut self, pkg: &Package) -> Result<()> { @@ -241,6 +303,7 @@ impl WitPrinter { let my_pkg = match owner { TypeOwner::Interface(id) => resolve.interfaces[id].package.unwrap(), TypeOwner::World(id) => resolve.worlds[id].package.unwrap(), + TypeOwner::Package(id) => id, TypeOwner::None => unreachable!(), }; for (owner, ty, tys) in types_to_import { @@ -249,29 +312,42 @@ impl WitPrinter { self.print_external_id(ty.external_id.as_deref()); self.output.keyword("use"); self.output.str(" "); - let id = match owner { - TypeOwner::Interface(id) => id, - // it's only possible to import types from interfaces at - // this time. - _ => unreachable!(), - }; - self.print_path_to_interface(resolve, id, my_pkg)?; - self.output.str(".{"); // Note: not changing the indentation. - for (i, (my_name, other_name)) in tys.into_iter().enumerate() { - if i > 0 { - self.output.str(", "); + match owner { + TypeOwner::Interface(id) => { + self.print_path_to_interface(resolve, id, my_pkg)?; + self.output.str(".{"); // Note: not changing the indentation. + for (i, (my_name, other_name)) in tys.into_iter().enumerate() { + if i > 0 { + self.output.str(", "); + } + if my_name == other_name { + self.print_name_type(my_name, TypeKind::TypeImport); + } else { + self.print_name_type(other_name, TypeKind::TypeImport); + self.output.str(" "); + self.output.keyword("as"); + self.output.str(" "); + self.print_name_type(my_name, TypeKind::TypeAlias); + } + } + self.output.str("}"); // Note: not changing the indentation. } - if my_name == other_name { - self.print_name_type(my_name, TypeKind::TypeImport); - } else { - self.print_name_type(other_name, TypeKind::TypeImport); - self.output.str(" "); - self.output.keyword("as"); - self.output.str(" "); - self.print_name_type(my_name, TypeKind::TypeAlias); + TypeOwner::Package(pkg) => { + // Package-scope types are imported with toplevel `use`. + assert_eq!(tys.len(), 1); + let (my_name, other_name) = tys[0]; + self.print_path_to_package_type(resolve, pkg, other_name, my_pkg)?; + if my_name != other_name { + self.output.str(" "); + self.output.keyword("as"); + self.output.str(" "); + self.print_name_type(my_name, TypeKind::TypeAlias); + } } + // it's only possible to import types from interfaces or + // packages at this time. + _ => unreachable!(), } - self.output.str("}"); // Note: not changing the indentation. self.output.semicolon(); } @@ -556,6 +632,29 @@ impl WitPrinter { Ok(()) } + fn print_path_to_package_type( + &mut self, + resolve: &Resolve, + package: PackageId, + type_name: &str, + cur_pkg: PackageId, + ) -> Result<()> { + if package == cur_pkg { + self.print_name_type(type_name, TypeKind::TypeImport); + } else { + let pkg = &resolve.packages[package].name; + self.print_name_type(&pkg.namespace, TypeKind::NamespacePath); + self.output.str(":"); + self.print_name_type(&pkg.name, TypeKind::PackageNamePath); + self.output.str("/"); + self.print_name_type(type_name, TypeKind::TypeImport); + if let Some(version) = &pkg.version { + self.print_name_type(&format!("@{version}"), TypeKind::VersionPath); + } + } + Ok(()) + } + /// Print the name of type `ty`. pub fn print_type_name(&mut self, resolve: &Resolve, ty: &Type) -> Result<()> { match ty { diff --git a/crates/wit-component/tests/interfaces/package-scope-docs.wat b/crates/wit-component/tests/interfaces/package-scope-docs.wat new file mode 100644 index 0000000000..07af1d25d5 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-docs.wat @@ -0,0 +1,41 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type 0)) + (type (;2;) + (component + (type (;0;) + (instance + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type (eq 0))) + (type (;2;) (func (param "p" 1))) + (export (;0;) "move-to" (func (type 2))) + ) + ) + (export (;0;) "local:demo/api" (instance (type 0))) + ) + ) + (export (;3;) "api" (type 2)) + (type (;4;) + (component + (type (;0;) + (component + (type (;0;) + (instance + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type (eq 0))) + (type (;2;) (func (param "p" 1))) + (export (;0;) "move-to" (func (type 2))) + ) + ) + (export (;0;) "local:demo/api" (instance (type 0))) + ) + ) + (export (;0;) "local:demo/w" (component (type 0))) + ) + ) + (export (;5;) "w" (type 4)) + (@custom "package-docs" "\01{\22interfaces\22:{\22api\22:{\22funcs\22:{\22move-to\22:{\22docs\22:\22Move to a package-scope point.\22}}}},\22types\22:{\22point\22:{\22docs\22:\22A shared point type at package scope.\22}}}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-docs.wit b/crates/wit-component/tests/interfaces/package-scope-docs.wit new file mode 100644 index 0000000000..457ad34c16 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-docs.wit @@ -0,0 +1,16 @@ +package local:demo; + +/// A shared point type at package scope. +record point { + x: u32, + y: u32, +} + +interface api { + /// Move to a package-scope point. + move-to: func(p: point); +} + +world w { + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-docs.wit.print b/crates/wit-component/tests/interfaces/package-scope-docs.wit.print new file mode 100644 index 0000000000..457ad34c16 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-docs.wit.print @@ -0,0 +1,16 @@ +package local:demo; + +/// A shared point type at package scope. +record point { + x: u32, + y: u32, +} + +interface api { + /// Move to a package-scope point. + move-to: func(p: point); +} + +world w { + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-field-docs.wat b/crates/wit-component/tests/interfaces/package-scope-field-docs.wat new file mode 100644 index 0000000000..8a941ce350 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-field-docs.wat @@ -0,0 +1,63 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type 0)) + (type (;2;) (variant (case "circle") (case "rect" 1))) + (export (;3;) "shape" (type 2)) + (type (;4;) (flags "bold" "italic")) + (export (;5;) "style" (type 4)) + (type (;6;) (enum "north" "south")) + (export (;7;) "direction" (type 6)) + (type (;8;) + (component + (type (;0;) + (instance + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type (eq 0))) + (type (;2;) (variant (case "circle") (case "rect" 1))) + (export (;3;) "shape" (type (eq 2))) + (type (;4;) (flags "bold" "italic")) + (export (;5;) "style" (type (eq 4))) + (type (;6;) (func (param "s" 3) (param "style" 5))) + (export (;0;) "draw" (func (type 6))) + (type (;7;) (enum "north" "south")) + (export (;8;) "direction" (type (eq 7))) + (type (;9;) (func (result 8))) + (export (;1;) "heading" (func (type 9))) + ) + ) + (export (;0;) "local:demo/api" (instance (type 0))) + ) + ) + (export (;9;) "api" (type 8)) + (type (;10;) + (component + (type (;0;) + (component + (type (;0;) + (instance + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type (eq 0))) + (type (;2;) (variant (case "circle") (case "rect" 1))) + (export (;3;) "shape" (type (eq 2))) + (type (;4;) (flags "bold" "italic")) + (export (;5;) "style" (type (eq 4))) + (type (;6;) (func (param "s" 3) (param "style" 5))) + (export (;0;) "draw" (func (type 6))) + (type (;7;) (enum "north" "south")) + (export (;8;) "direction" (type (eq 7))) + (type (;9;) (func (result 8))) + (export (;1;) "heading" (func (type 9))) + ) + ) + (export (;0;) "local:demo/api" (instance (type 0))) + ) + ) + (export (;0;) "local:demo/w" (component (type 0))) + ) + ) + (export (;11;) "w" (type 10)) + (@custom "package-docs" "\01{\22types\22:{\22point\22:{\22docs\22:\22A shared point type at package scope.\22,\22items\22:{\22x\22:\22X coordinate\22,\22y\22:\22Y coordinate\22}},\22shape\22:{\22docs\22:\22Geometric shape using package-scope point.\22,\22items\22:{\22circle\22:\22Unit circle case\22,\22rect\22:\22Rectangle backed by a point\22}},\22style\22:{\22items\22:{\22bold\22:\22Bold text\22}},\22direction\22:{\22items\22:{\22north\22:\22Facing north\22}}}}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-field-docs.wit b/crates/wit-component/tests/interfaces/package-scope-field-docs.wit new file mode 100644 index 0000000000..b873f7dcb7 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-field-docs.wit @@ -0,0 +1,38 @@ +package local:demo; + +/// A shared point type at package scope. +record point { + /// X coordinate + x: u32, + /// Y coordinate + y: u32, +} + +/// Geometric shape using package-scope point. +variant shape { + /// Unit circle case + circle, + /// Rectangle backed by a point + rect(point), +} + +flags style { + /// Bold text + bold, + italic, +} + +enum direction { + /// Facing north + north, + south, +} + +interface api { + draw: func(s: shape, style: style); + heading: func() -> direction; +} + +world w { + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-field-docs.wit.print b/crates/wit-component/tests/interfaces/package-scope-field-docs.wit.print new file mode 100644 index 0000000000..207faf9dbc --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-field-docs.wit.print @@ -0,0 +1,39 @@ +package local:demo; + +/// A shared point type at package scope. +record point { + /// X coordinate + x: u32, + /// Y coordinate + y: u32, +} + +/// Geometric shape using package-scope point. +variant shape { + /// Unit circle case + circle, + /// Rectangle backed by a point + rect(point), +} + +flags style { + /// Bold text + bold, + italic, +} + +enum direction { + /// Facing north + north, + south, +} + +interface api { + draw: func(s: shape, style: style); + + heading: func() -> direction; +} + +world w { + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-foreign-use.wat b/crates/wit-component/tests/interfaces/package-scope-foreign-use.wat new file mode 100644 index 0000000000..ffb2390b66 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-foreign-use.wat @@ -0,0 +1,51 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type 0)) + (type (;2;) (record (field "p" 1))) + (export (;3;) "bin" (type 2)) + (type (;4;) + (component + (type (;0;) + (instance + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type (eq 0))) + (type (;2;) (record (field "p" 1))) + (export (;3;) "bin" (type (eq 2))) + (type (;4;) (func (param "b" 3))) + (export (;0;) "wrap" (func (type 4))) + (type (;5;) (func (param "p" 1))) + (export (;1;) "move-to" (func (type 5))) + ) + ) + (export (;0;) "local:consumer/api" (instance (type 0))) + ) + ) + (export (;5;) "api" (type 4)) + (type (;6;) + (component + (type (;0;) + (component + (type (;0;) + (instance + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type (eq 0))) + (type (;2;) (record (field "p" 1))) + (export (;3;) "bin" (type (eq 2))) + (type (;4;) (func (param "b" 3))) + (export (;0;) "wrap" (func (type 4))) + (type (;5;) (func (param "p" 1))) + (export (;1;) "move-to" (func (type 5))) + ) + ) + (export (;0;) "local:consumer/api" (instance (type 0))) + ) + ) + (export (;0;) "local:consumer/w" (component (type 0))) + ) + ) + (export (;7;) "w" (type 6)) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit b/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit new file mode 100644 index 0000000000..a1703d92f3 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit @@ -0,0 +1,16 @@ +package local:consumer; + +use local:types/point; + +record bin { + p: point, +} + +interface api { + wrap: func(b: bin); + move-to: func(p: point); +} + +world w { + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit.print b/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit.print new file mode 100644 index 0000000000..b27ded5342 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit.print @@ -0,0 +1,20 @@ +package local:consumer; + +record point { + x: u32, + y: u32, +} + +record bin { + p: point, +} + +interface api { + wrap: func(b: bin); + + move-to: func(p: point); +} + +world w { + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-foreign-use/deps/types/types.wit b/crates/wit-component/tests/interfaces/package-scope-foreign-use/deps/types/types.wit new file mode 100644 index 0000000000..d25c1fd54a --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-foreign-use/deps/types/types.wit @@ -0,0 +1,10 @@ +package local:types; + +/// Shared package-scope point. +record point { + x: u32, + y: u32, +} + +interface unused { +} diff --git a/crates/wit-component/tests/interfaces/package-scope-nested.wat b/crates/wit-component/tests/interfaces/package-scope-nested.wat new file mode 100644 index 0000000000..f7caec2f00 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-nested.wat @@ -0,0 +1,41 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type 0)) + (type (;2;) + (component + (type (;0;) + (instance + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type (eq 0))) + (type (;2;) (func (param "p" 1))) + (export (;0;) "move-to" (func (type 2))) + ) + ) + (export (;0;) "local:nested/api" (instance (type 0))) + ) + ) + (export (;3;) "api" (type 2)) + (type (;4;) + (component + (type (;0;) + (component + (type (;0;) + (instance + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type (eq 0))) + (type (;2;) (func (param "p" 1))) + (export (;0;) "move-to" (func (type 2))) + ) + ) + (export (;0;) "local:nested/api" (instance (type 0))) + ) + ) + (export (;0;) "local:nested/w" (component (type 0))) + ) + ) + (export (;5;) "w" (type 4)) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-nested.wit b/crates/wit-component/tests/interfaces/package-scope-nested.wit new file mode 100644 index 0000000000..6bf8a55f65 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-nested.wit @@ -0,0 +1,14 @@ +package local:nested; + +record point { + x: u32, + y: u32, +} + +interface api { + move-to: func(p: point); +} + +world w { + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-nested.wit.print b/crates/wit-component/tests/interfaces/package-scope-nested.wit.print new file mode 100644 index 0000000000..6bf8a55f65 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-nested.wit.print @@ -0,0 +1,14 @@ +package local:nested; + +record point { + x: u32, + y: u32, +} + +interface api { + move-to: func(p: point); +} + +world w { + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-types.wat b/crates/wit-component/tests/interfaces/package-scope-types.wat new file mode 100644 index 0000000000..417e8aa2e6 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-types.wat @@ -0,0 +1,86 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type 0)) + (type (;2;) (variant (case "circle" u32) (case "rect" 1))) + (export (;3;) "shape" (type 2)) + (type (;4;) (flags "bold" "italic")) + (export (;5;) "style" (type 4)) + (type (;6;) (enum "north" "south" "east" "west")) + (export (;7;) "direction" (type 6)) + (type (;8;) (list 1)) + (export (;9;) "path" (type 8)) + (type (;10;) + (component + (type (;0;) + (instance + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type (eq 0))) + (type (;2;) (func (param "p" 1))) + (export (;0;) "move-to" (func (type 2))) + (type (;3;) (enum "north" "south" "east" "west")) + (export (;4;) "direction" (type (eq 3))) + (type (;5;) (func (result 4))) + (export (;1;) "heading" (func (type 5))) + (type (;6;) (list 1)) + (export (;7;) "path" (type (eq 6))) + (type (;8;) (func (result 7))) + (export (;2;) "trail" (func (type 8))) + (type (;9;) (variant (case "circle" u32) (case "rect" 1))) + (export (;10;) "shape" (type (eq 9))) + (type (;11;) (flags "bold" "italic")) + (export (;12;) "style" (type (eq 11))) + (type (;13;) (func (param "s" 10) (param "style" 12))) + (export (;3;) "draw" (func (type 13))) + ) + ) + (export (;0;) "local:demo/api" (instance (type 0))) + ) + ) + (export (;11;) "api" (type 10)) + (type (;12;) + (component + (type (;0;) + (component + (type (;0;) + (instance + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type (eq 0))) + (type (;2;) (func (param "p" 1))) + (export (;0;) "place" (func (type 2))) + ) + ) + (import "host" (instance (;0;) (type 0))) + (type (;1;) + (instance + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type (eq 0))) + (type (;2;) (func (param "p" 1))) + (export (;0;) "move-to" (func (type 2))) + (type (;3;) (enum "north" "south" "east" "west")) + (export (;4;) "direction" (type (eq 3))) + (type (;5;) (func (result 4))) + (export (;1;) "heading" (func (type 5))) + (type (;6;) (list 1)) + (export (;7;) "path" (type (eq 6))) + (type (;8;) (func (result 7))) + (export (;2;) "trail" (func (type 8))) + (type (;9;) (variant (case "circle" u32) (case "rect" 1))) + (export (;10;) "shape" (type (eq 9))) + (type (;11;) (flags "bold" "italic")) + (export (;12;) "style" (type (eq 11))) + (type (;13;) (func (param "s" 10) (param "style" 12))) + (export (;3;) "draw" (func (type 13))) + ) + ) + (export (;1;) "local:demo/api" (instance (type 1))) + ) + ) + (export (;0;) "local:demo/w" (component (type 0))) + ) + ) + (export (;13;) "w" (type 12)) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-types.wit b/crates/wit-component/tests/interfaces/package-scope-types.wit new file mode 100644 index 0000000000..9cfceb3d33 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-types.wit @@ -0,0 +1,39 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +variant shape { + circle(u32), + rect(point), +} + +flags style { + bold, + italic, +} + +enum direction { + north, + south, + east, + west, +} + +type path = list; + +interface api { + move-to: func(p: point); + heading: func() -> direction; + trail: func() -> path; + draw: func(s: shape, style: style); +} + +world w { + import host: interface { + place: func(p: point); + } + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-types.wit.print b/crates/wit-component/tests/interfaces/package-scope-types.wit.print new file mode 100644 index 0000000000..bba812babf --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-types.wit.print @@ -0,0 +1,43 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +variant shape { + circle(u32), + rect(point), +} + +flags style { + bold, + italic, +} + +enum direction { + north, + south, + east, + west, +} + +type path = list; + +interface api { + move-to: func(p: point); + + heading: func() -> direction; + + trail: func() -> path; + + draw: func(s: shape, style: style); +} + +world w { + import host: interface { + place: func(p: point); + } + + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-world-only.wat b/crates/wit-component/tests/interfaces/package-scope-world-only.wat new file mode 100644 index 0000000000..f1ae208e15 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-world-only.wat @@ -0,0 +1,26 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type 0)) + (type (;2;) + (component + (type (;0;) + (component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "point" (type (;1;) (eq 0))) + (type (;2;) (func (param "p" 1))) + (import "move-to" (func (;0;) (type 2))) + (export (;1;) "place" (func (type 2))) + (type (;3;) (list 1)) + (type (;4;) (func (result 3))) + (export (;2;) "trail" (func (type 4))) + ) + ) + (export (;0;) "local:demo/w" (component (type 0))) + ) + ) + (export (;3;) "w" (type 2)) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-world-only.wit b/crates/wit-component/tests/interfaces/package-scope-world-only.wit new file mode 100644 index 0000000000..3a7b29968d --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-world-only.wit @@ -0,0 +1,12 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +world w { + import move-to: func(p: point); + export place: func(p: point); + export trail: func() -> list; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-world-only.wit.print b/crates/wit-component/tests/interfaces/package-scope-world-only.wit.print new file mode 100644 index 0000000000..3b58727f3e --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-world-only.wit.print @@ -0,0 +1,13 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +world w { + import move-to: func(p: point); + + export place: func(p: point); + export trail: func() -> list; +} diff --git a/crates/wit-dylib/tests/roundtrip.rs b/crates/wit-dylib/tests/roundtrip.rs index 59a40adc3e..f2e89744cf 100644 --- a/crates/wit-dylib/tests/roundtrip.rs +++ b/crates/wit-dylib/tests/roundtrip.rs @@ -105,6 +105,7 @@ fn run_one(u: &mut Unstructured<'_>) -> Result<()> { }, interfaces: Default::default(), worlds: Default::default(), + types: Default::default(), docs: Default::default(), }); diff --git a/crates/wit-encoder/src/from_parser.rs b/crates/wit-encoder/src/from_parser.rs index a757d17841..340e6df5bc 100644 --- a/crates/wit-encoder/src/from_parser.rs +++ b/crates/wit-encoder/src/from_parser.rs @@ -30,6 +30,33 @@ impl<'a> Converter<'a> { fn convert_package(&self, package_id: PackageId, package: &wit_parser::Package) -> Package { let mut output = Package::new(self.convert_package_name(&package.name)); + // Emit toplevel `use` for foreign package-scope types referenced by + // this package's types, interfaces, and worlds. + let mut foreign_uses = Vec::new(); + for (_, id) in &package.types { + self.collect_foreign_package_type_uses(package_id, *id, &mut foreign_uses); + } + for (_, id) in &package.interfaces { + let interface = self.resolve.interfaces.get(*id).unwrap(); + self.collect_foreign_package_type_uses_from_interface( + package_id, + interface, + &mut foreign_uses, + ); + } + for (_, id) in &package.worlds { + let world = self.resolve.worlds.get(*id).unwrap(); + self.collect_foreign_package_type_uses_from_world(package_id, world, &mut foreign_uses); + } + for path in foreign_uses { + output.use_type(path, None); + } + for (_, id) in &package.types { + let type_def = self.resolve.types.get(*id).unwrap(); + if let Some(converted) = self.convert_type_def(type_def, *id) { + output.type_def(converted); + } + } for (_, id) in &package.interfaces { let interface = self.resolve.interfaces.get(*id).unwrap(); output.interface(self.convert_interface( @@ -384,7 +411,9 @@ impl<'a> Converter<'a> { wit_parser::TypeOwner::Interface(id) => { &self.resolve.interfaces.get(*id).unwrap().functions } - wit_parser::TypeOwner::None => panic!("Resource has to have an owner"), + wit_parser::TypeOwner::Package(_) | wit_parser::TypeOwner::None => { + panic!("Resource has to have an owner") + } }; let mut output = Resource::empty(); @@ -578,6 +607,153 @@ impl<'a> Converter<'a> { None } } + + fn package_type_path(&self, package_id: PackageId, type_name: &str) -> Ident { + let package = self.resolve.packages.get(package_id).unwrap(); + Ident::new(format!( + "{}:{}/{}{}", + package.name.namespace, + package.name.name, + type_name, + package + .name + .version + .as_ref() + .map(|version| format!("@{version}")) + .unwrap_or_default() + )) + } + + fn collect_foreign_package_type_uses( + &self, + package_id: PackageId, + type_id: wit_parser::TypeId, + uses: &mut Vec, + ) { + let type_def = self.resolve.types.get(type_id).unwrap(); + match &type_def.kind { + wit_parser::TypeDefKind::Type(ty) + | wit_parser::TypeDefKind::List(ty) + | wit_parser::TypeDefKind::FixedLengthList(ty, _) + | wit_parser::TypeDefKind::Option(ty) + | wit_parser::TypeDefKind::Future(Some(ty)) + | wit_parser::TypeDefKind::Stream(Some(ty)) => { + self.collect_foreign_package_type_uses_from_type(package_id, ty, uses); + } + wit_parser::TypeDefKind::Map(k, v) => { + self.collect_foreign_package_type_uses_from_type(package_id, k, uses); + self.collect_foreign_package_type_uses_from_type(package_id, v, uses); + } + wit_parser::TypeDefKind::Result(r) => { + if let Some(ty) = &r.ok { + self.collect_foreign_package_type_uses_from_type(package_id, ty, uses); + } + if let Some(ty) = &r.err { + self.collect_foreign_package_type_uses_from_type(package_id, ty, uses); + } + } + wit_parser::TypeDefKind::Tuple(t) => { + for ty in &t.types { + self.collect_foreign_package_type_uses_from_type(package_id, ty, uses); + } + } + wit_parser::TypeDefKind::Record(r) => { + for field in &r.fields { + self.collect_foreign_package_type_uses_from_type(package_id, &field.ty, uses); + } + } + wit_parser::TypeDefKind::Variant(v) => { + for case in &v.cases { + if let Some(ty) = &case.ty { + self.collect_foreign_package_type_uses_from_type(package_id, ty, uses); + } + } + } + wit_parser::TypeDefKind::Handle(h) => { + let id = match h { + wit_parser::Handle::Own(id) | wit_parser::Handle::Borrow(id) => *id, + }; + self.collect_foreign_package_type_uses(package_id, id, uses); + } + wit_parser::TypeDefKind::Enum(_) + | wit_parser::TypeDefKind::Flags(_) + | wit_parser::TypeDefKind::Resource + | wit_parser::TypeDefKind::Future(None) + | wit_parser::TypeDefKind::Stream(None) + | wit_parser::TypeDefKind::Unknown => {} + } + } + + fn collect_foreign_package_type_uses_from_type( + &self, + package_id: PackageId, + ty: &wit_parser::Type, + uses: &mut Vec, + ) { + let wit_parser::Type::Id(id) = ty else { + return; + }; + let type_def = self.resolve.types.get(*id).unwrap(); + if let wit_parser::TypeOwner::Package(owner) = type_def.owner { + if owner != package_id { + let name = type_def.name.as_deref().expect("package type must be named"); + let path = self.package_type_path(owner, name); + if !uses.contains(&path) { + uses.push(path); + } + return; + } + } + self.collect_foreign_package_type_uses(package_id, *id, uses); + } + + fn collect_foreign_package_type_uses_from_interface( + &self, + package_id: PackageId, + interface: &wit_parser::Interface, + uses: &mut Vec, + ) { + for (_, type_id) in &interface.types { + self.collect_foreign_package_type_uses(package_id, *type_id, uses); + } + for (_, func) in &interface.functions { + for ty in func.parameter_and_result_types() { + self.collect_foreign_package_type_uses_from_type(package_id, &ty, uses); + } + } + } + + fn collect_foreign_package_type_uses_from_world( + &self, + package_id: PackageId, + world: &wit_parser::World, + uses: &mut Vec, + ) { + for (_, item) in world.imports.iter().chain(world.exports.iter()) { + match item { + wit_parser::WorldItem::Function(func) => { + for ty in func.parameter_and_result_types() { + self.collect_foreign_package_type_uses_from_type(package_id, &ty, uses); + } + } + wit_parser::WorldItem::Type { id, .. } => { + self.collect_foreign_package_type_uses(package_id, *id, uses); + } + wit_parser::WorldItem::Interface { id, .. } => { + let interface = self.resolve.interfaces.get(*id).unwrap(); + // Named interfaces from other packages are imported by name; + // only scan anonymous/inline interfaces owned here. + if interface.package == Some(package_id) && interface.name.is_none() { + self.collect_foreign_package_type_uses_from_interface( + package_id, + interface, + uses, + ); + } + } + } + } + } } fn clean_func_name(resource_name: &str, method_name: &str) -> String { diff --git a/crates/wit-encoder/src/package.rs b/crates/wit-encoder/src/package.rs index c6606cf09f..37f18c19f9 100644 --- a/crates/wit-encoder/src/package.rs +++ b/crates/wit-encoder/src/package.rs @@ -3,7 +3,7 @@ use std::ops::{Deref, DerefMut}; use semver::Version; -use crate::{Interface, Render, RenderOpts, World, ident::Ident}; +use crate::{Interface, Render, RenderOpts, TypeDef, World, ident::Ident}; /// A WIT package. /// @@ -48,6 +48,17 @@ impl Package { self.items.push(PackageItem::World(world)) } + /// Add a package-scope type to the package + pub fn type_def(&mut self, type_def: TypeDef) { + self.items.push(PackageItem::Type(type_def)) + } + + /// Add a toplevel `use` (e.g. `use ns:pkg/name;`) for a foreign package-scope type. + pub fn use_type(&mut self, path: impl Into, alias: Option) { + self.items + .push(PackageItem::Use(ToplevelUse::new(path, alias))); + } + pub fn item(&mut self, item: impl Into) { self.items.push(item.into()); } @@ -82,6 +93,12 @@ impl Package { PackageItem::World(world) => { world.render(f, opts)?; } + PackageItem::Type(type_def) => { + type_def.render(f, opts)?; + } + PackageItem::Use(use_) => { + use_.render(f, opts)?; + } } } Ok(()) @@ -147,6 +164,45 @@ impl fmt::Display for NestedPackage { pub enum PackageItem { Interface(Interface), World(World), + Type(TypeDef), + /// Toplevel `use ns:pkg/name;` for a foreign package-scope type. + Use(ToplevelUse), +} + +/// A toplevel WIT `use` of a package-scope type (`use ns:pkg/name [as alias];`). +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))] +pub struct ToplevelUse { + path: Ident, + alias: Option, +} + +impl ToplevelUse { + pub fn new(path: impl Into, alias: Option) -> Self { + Self { + path: path.into(), + alias, + } + } + + pub fn path(&self) -> &Ident { + &self.path + } + + pub fn alias(&self) -> Option<&Ident> { + self.alias.as_ref() + } +} + +impl Render for ToplevelUse { + fn render(&self, f: &mut fmt::Formatter<'_>, opts: &RenderOpts) -> fmt::Result { + write!(f, "{}use {}", opts.spaces(), self.path)?; + if let Some(alias) = &self.alias { + write!(f, " as {alias}")?; + } + write!(f, ";\n") + } } /// A structure used to keep track of the name of a package, containing optional diff --git a/crates/wit-encoder/tests/package-scope-types.rs b/crates/wit-encoder/tests/package-scope-types.rs new file mode 100644 index 0000000000..4d3213d59a --- /dev/null +++ b/crates/wit-encoder/tests/package-scope-types.rs @@ -0,0 +1,314 @@ +use pretty_assertions::assert_eq; +use wit_encoder::packages_from_parsed; + +const WIT: &str = r#"package local:demo; + +/// Shared point. +record point { + /// X coordinate + x: u32, + y: u32, +} + +flags style { + bold, + italic, +} + +interface api { + move-to: func(p: point); + style: func() -> style; +} + +world w { + export api; +} +"#; + +#[test] +fn package_scope_types_round_trip() { + let mut resolve = wit_parser::Resolve::new(); + resolve.push_str("demo.wit", WIT).unwrap(); + let packages = packages_from_parsed(&resolve); + assert_eq!(packages.len(), 1); + let rendered = packages[0].to_string(); + assert!( + rendered.contains("record point"), + "expected package-scope record in encoder output:\n{rendered}" + ); + assert!( + rendered.contains("flags style"), + "expected package-scope flags in encoder output:\n{rendered}" + ); + + let mut resolve2 = wit_parser::Resolve::new(); + resolve2.push_str("demo.wit", &rendered).unwrap(); + let packages2 = packages_from_parsed(&resolve2); + assert_eq!(packages[0].to_string(), packages2[0].to_string()); +} + +#[test] +fn package_scope_types_order() { + let mut resolve = wit_parser::Resolve::new(); + resolve.push_str("demo.wit", WIT).unwrap(); + let packages = packages_from_parsed(&resolve); + let rendered = packages[0].to_string(); + + let point = rendered.find("record point").expect("missing point"); + let style = rendered.find("flags style").expect("missing style"); + let api = rendered.find("interface api").expect("missing api"); + let world = rendered.find("world w").expect("missing world"); + assert!( + point < style && style < api && api < world, + "expected package types before interfaces/worlds:\n{rendered}" + ); +} + +#[test] +fn package_scope_foreign_compose_round_trip() { + let mut resolve = wit_parser::Resolve::new(); + resolve + .push_str( + "types.wit", + r#" +package local:types; + +record point { + x: u32, + y: u32, +} + +interface unused {} +"#, + ) + .unwrap(); + resolve + .push_str( + "consumer.wit", + r#" +package local:consumer; + +use local:types/point; + +record bin { + p: point, +} + +interface api { + wrap: func(b: bin); +} + +world w { + export api; +} +"#, + ) + .unwrap(); + + let packages = packages_from_parsed(&resolve); + let consumer = packages + .iter() + .find(|p| p.name().to_string() == "local:consumer") + .expect("consumer package"); + let rendered = consumer.to_string(); + assert!( + rendered.contains("use local:types/point;"), + "expected toplevel use of foreign package type:\n{rendered}" + ); + assert!( + rendered.contains("record bin"), + "expected consumer package-scope type:\n{rendered}" + ); + + // Push deps first, then consumer. + let mut resolve2 = wit_parser::Resolve::new(); + let types = packages + .iter() + .find(|p| p.name().to_string() == "local:types") + .unwrap(); + resolve2 + .push_str("types.wit", &types.to_string()) + .unwrap(); + resolve2 + .push_str("consumer.wit", &rendered) + .unwrap(); + let packages2 = packages_from_parsed(&resolve2); + let rendered2 = packages2 + .iter() + .find(|p| p.name().to_string() == "local:consumer") + .unwrap() + .to_string(); + assert_eq!(rendered, rendered2); +} + +#[test] +fn package_scope_nested_round_trip() { + let wit = r#"package local:nested; + +record point { + x: u32, + y: u32, +} + +interface api { + move-to: func(p: point); +} + +world w { + export api; +} +"#; + let mut resolve = wit_parser::Resolve::new(); + resolve.push_str("nested.wit", wit).unwrap(); + let packages = packages_from_parsed(&resolve); + assert_eq!(packages.len(), 1); + let rendered = packages[0].to_string(); + assert!( + rendered.contains("record point"), + "expected nested package-scope type:\n{rendered}" + ); + + let mut resolve2 = wit_parser::Resolve::new(); + resolve2.push_str("nested.wit", &rendered).unwrap(); + let packages2 = packages_from_parsed(&resolve2); + assert_eq!(packages[0].to_string(), packages2[0].to_string()); +} + +#[test] +fn package_scope_foreign_iface_only_round_trip() { + let mut resolve = wit_parser::Resolve::new(); + resolve + .push_str( + "types.wit", + r#" +package local:types; + +record point { + x: u32, + y: u32, +} + +interface unused {} +"#, + ) + .unwrap(); + resolve + .push_str( + "consumer.wit", + r#" +package local:consumer; + +use local:types/point; + +interface api { + move-to: func(p: point); +} + +world w { + export api; +} +"#, + ) + .unwrap(); + + let packages = packages_from_parsed(&resolve); + let consumer = packages + .iter() + .find(|p| p.name().to_string() == "local:consumer") + .expect("consumer package"); + let rendered = consumer.to_string(); + assert!( + rendered.contains("use local:types/point;"), + "iface-only foreign refs must still emit toplevel use:\n{rendered}" + ); + assert!( + !rendered.contains("record point"), + "consumer should not redefine foreign package type:\n{rendered}" + ); + + let mut resolve2 = wit_parser::Resolve::new(); + let types = packages + .iter() + .find(|p| p.name().to_string() == "local:types") + .unwrap(); + resolve2 + .push_str("types.wit", &types.to_string()) + .unwrap(); + resolve2 + .push_str("consumer.wit", &rendered) + .unwrap(); + let packages2 = packages_from_parsed(&resolve2); + let rendered2 = packages2 + .iter() + .find(|p| p.name().to_string() == "local:consumer") + .unwrap() + .to_string(); + assert_eq!(rendered, rendered2); +} + +#[test] +fn package_scope_foreign_world_only_round_trip() { + let mut resolve = wit_parser::Resolve::new(); + resolve + .push_str( + "types.wit", + r#" +package local:types; + +record point { + x: u32, + y: u32, +} + +interface unused {} +"#, + ) + .unwrap(); + resolve + .push_str( + "consumer.wit", + r#" +package local:consumer; + +use local:types/point; + +world w { + import move-to: func(p: point); + export place: func(p: point); +} +"#, + ) + .unwrap(); + + let packages = packages_from_parsed(&resolve); + let consumer = packages + .iter() + .find(|p| p.name().to_string() == "local:consumer") + .expect("consumer package"); + let rendered = consumer.to_string(); + assert!( + rendered.contains("use local:types/point;"), + "world-only foreign refs must still emit toplevel use:\n{rendered}" + ); + + let mut resolve2 = wit_parser::Resolve::new(); + let types = packages + .iter() + .find(|p| p.name().to_string() == "local:types") + .unwrap(); + resolve2 + .push_str("types.wit", &types.to_string()) + .unwrap(); + resolve2 + .push_str("consumer.wit", &rendered) + .unwrap(); + let packages2 = packages_from_parsed(&resolve2); + assert_eq!( + consumer.to_string(), + packages2 + .iter() + .find(|p| p.name().to_string() == "local:consumer") + .unwrap() + .to_string() + ); +} diff --git a/crates/wit-parser/src/ast.rs b/crates/wit-parser/src/ast.rs index 10ed687411..766b3be58b 100644 --- a/crates/wit-parser/src/ast.rs +++ b/crates/wit-parser/src/ast.rs @@ -1,6 +1,6 @@ use crate::ast::error::ParseError; use crate::{ParseResult, UnresolvedPackage, UnresolvedPackageGroup}; -use alloc::borrow::Cow; +use alloc::borrow::{Cow, ToOwned}; use alloc::boxed::Box; use alloc::format; use alloc::string::{String, ToString}; @@ -259,6 +259,9 @@ impl<'a> DeclList<'a> { )?; } + // Package-scope types only reference names, not package paths. + AstItem::Type(_) => {} + AstItem::Package(pkg) => pkg.decl_list.for_each_path(f)?, } } @@ -271,6 +274,7 @@ enum AstItem<'a> { World(World<'a>), Use(ToplevelUse<'a>), Package(PackageFile<'a>), + Type(TypeDef<'a>), } impl<'a> AstItem<'a> { @@ -285,7 +289,31 @@ impl<'a> AstItem<'a> { Some((_span, Token::Package)) => { PackageFile::parse_nested(tokens, docs, attributes, depth).map(Self::Package) } - other => Err(err_expected(tokens, "`world`, `interface` or `use`", other).into()), + Some((_span, Token::Type)) => { + TypeDef::parse(tokens, docs, attributes).map(Self::Type) + } + Some((_span, Token::Flags)) => { + TypeDef::parse_flags(tokens, docs, attributes).map(Self::Type) + } + Some((_span, Token::Record)) => { + TypeDef::parse_record(tokens, docs, attributes).map(Self::Type) + } + Some((_span, Token::Variant)) => { + TypeDef::parse_variant(tokens, docs, attributes).map(Self::Type) + } + Some((_span, Token::Enum)) => { + TypeDef::parse_enum(tokens, docs, attributes).map(Self::Type) + } + Some((span, Token::Resource)) => Err(ParseError::new_syntax( + span, + "resources cannot be declared at package scope".to_owned(), + )), + other => Err(err_expected( + tokens, + "`world`, `interface`, `use`, or type definition", + other, + ) + .into()), } } } diff --git a/crates/wit-parser/src/ast/resolve.rs b/crates/wit-parser/src/ast/resolve.rs index 3ac115a27d..082e4a53e8 100644 --- a/crates/wit-parser/src/ast/resolve.rs +++ b/crates/wit-parser/src/ast/resolve.rs @@ -37,10 +37,23 @@ pub struct Resolver<'a> { /// handling things like `use` at the top level. ast_items: Vec>, + /// Per-file aliases from toplevel `use` that resolve (or may resolve) to + /// package-scope types. + ast_type_items: Vec>, + /// A map for the entire package being created of all names defined within, /// along with the ID they're mapping to. package_items: IndexMap<&'a str, AstItem>, + /// Package-scope types keyed by name. Owners are [`TypeOwner::None`] until + /// merge into a [`Resolve`]. + package_types: IndexMap<&'a str, TypeId>, + + /// Names of package-scope types discovered before they are allocated. + /// Used so top-level `use` of those names can be deferred until after + /// `resolve_package_types`. + pending_package_type_names: HashSet<&'a str>, + /// A per-interface map of name to item-in-the-interface. This is the same /// length as `self.types` and is pushed to whenever `self.types` is pushed /// to. @@ -54,6 +67,10 @@ pub struct Resolver<'a> { /// of its imports. foreign_deps: IndexMap)>>, + /// Dual stubs for toplevel `use ns:pkg/name` whose target may be an + /// interface or a package-scope type. + foreign_unknown: IndexMap<(PackageName, String), (InterfaceId, TypeId)>, + /// All interfaces that are present within `self.foreign_deps`. foreign_interfaces: HashSet, @@ -174,8 +191,14 @@ impl<'a> Resolver<'a> { // all interfaces in the package to visit. 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)?; + let (iface_order, world_order, mut ids) = self.populate_ast_items(&decl_lists)?; + // File namespaces for interfaces/worlds must exist before foreign type + // stubs are created (resolve_ast_item_path). Package-scope types are + // registered afterwards so Unknown stubs stay a prefix of the arena. + self.finish_ast_items(&decl_lists, &ids)?; self.populate_foreign_types(&decl_lists)?; + self.resolve_package_types(&decl_lists, &mut ids)?; + self.register_package_types_in_ns(&decl_lists, &ids)?; // Use the topological ordering of all interfaces to resolve all // interfaces in-order. Note that a reverse-mapping from ID to AST is @@ -188,18 +211,18 @@ impl<'a> Resolver<'a> { ast::AstItem::Interface(iface) => { let id = match self.ast_items[i][iface.name.name] { AstItem::Interface(id) => id, - AstItem::World(_) => unreachable!(), + AstItem::World(_) | AstItem::Type(_) => unreachable!(), }; iface_id_to_ast.insert(id, (iface, i)); } ast::AstItem::World(world) => { let id = match self.ast_items[i][world.name.name] { AstItem::World(id) => id, - AstItem::Interface(_) => unreachable!(), + AstItem::Interface(_) | AstItem::Type(_) => unreachable!(), }; world_id_to_ast.insert(id, (world, i)); } - ast::AstItem::Use(_) => {} + ast::AstItem::Use(_) | ast::AstItem::Type(_) => {} ast::AstItem::Package(_) => unreachable!(), } } @@ -225,6 +248,12 @@ impl<'a> Resolver<'a> { worlds: mem::take(&mut self.worlds), types: mem::take(&mut self.types), interfaces: mem::take(&mut self.interfaces), + package_types: self + .package_types + .iter() + .map(|(name, id)| (name.to_string(), *id)) + .collect(), + foreign_unknown: mem::take(&mut self.foreign_unknown), foreign_deps: self .foreign_deps .iter() @@ -253,6 +282,8 @@ impl<'a> Resolver<'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); + let mut foreign_unknown = mem::take(&mut self.foreign_unknown); + let self_pkg = self.package_name.as_ref().map(|(n, _)| n.clone()); for decl_list in decl_lists { decl_list .for_each_path(&mut |_, attrs, path, _names, world_or_iface| { @@ -261,41 +292,74 @@ impl<'a> Resolver<'a> { _ => return Ok(()), }; + // Same-package toplevel `use` of a name that may be a + // package-scope type is resolved locally later. Interface + // and world package paths still register (including self) + // so dependency cycles are detected. + if self_pkg.as_ref() == Some(&id.package_name()) + && matches!(world_or_iface, WorldOrInterface::Unknown) + { + return Ok(()); + } + let stability = self.stability(attrs)?; + let pkg_name = id.package_name(); - let deps = foreign_deps.entry(id.package_name()).or_insert_with(|| { + let deps = foreign_deps.entry(pkg_name.clone()).or_insert_with(|| { self.foreign_dep_spans.push(id.span); IndexMap::default() }); - let (id, stabilities) = deps.entry(name.name).or_insert_with(|| { - let id = match world_or_iface { + let (ast_item, stabilities) = deps.entry(name.name).or_insert_with(|| { + let ast_item = match world_or_iface { WorldOrInterface::World => { log::trace!( "creating a world for foreign dep: {}/{}", - id.package_name(), + pkg_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`. + WorldOrInterface::Interface => { log::trace!( "creating an interface for foreign dep: {}/{}", - id.package_name(), + pkg_name, name.name ); AstItem::Interface(self.alloc_interface(name.span)) } + WorldOrInterface::Unknown => { + // Dual stubs: may resolve to either an interface + // or a package-scope type once the dep is known. + log::trace!( + "creating dual stubs for foreign dep: {}/{}", + pkg_name, + name.name + ); + let iface = self.alloc_interface(name.span); + let ty = self.types.alloc(TypeDef { + docs: Docs::default(), + stability: Stability::Unknown, + kind: TypeDefKind::Unknown, + name: Some(name.name.to_string()), + owner: TypeOwner::None, + span: name.span, + external_id: None, + }); + self.unknown_type_spans.push(name.span); + foreign_unknown + .insert((pkg_name.clone(), name.name.to_string()), (iface, ty)); + AstItem::Interface(iface) + } }; - (id, Vec::new()) + (ast_item, Vec::new()) }); stabilities.push(stability); - let _ = match *id { + let _ = match *ast_item { AstItem::Interface(id) => foreign_interfaces.insert(id), AstItem::World(id) => foreign_worlds.insert(id), + AstItem::Type(_) => false, }; Ok(()) @@ -305,6 +369,7 @@ impl<'a> Resolver<'a> { self.foreign_deps = foreign_deps; self.foreign_interfaces = foreign_interfaces; self.foreign_worlds = foreign_worlds; + self.foreign_unknown = foreign_unknown; } fn alloc_interface(&mut self, span: Span) -> InterfaceId { @@ -340,7 +405,7 @@ impl<'a> Resolver<'a> { fn populate_ast_items( &mut self, decl_lists: &[ast::DeclList<'a>], - ) -> ParseResult<(Vec, Vec)> { + ) -> ParseResult<(Vec, Vec, IndexMap<&'a str, AstItem>)> { let mut package_items = IndexMap::default(); // Validate that all worlds and interfaces have unique names within this @@ -380,6 +445,21 @@ impl<'a> Resolver<'a> { let prev = names.insert(w.name.name, item); assert!(prev.is_none()); } + ast::AstItem::Type(t) => { + if package_items.insert(t.name.name, t.name.span).is_some() { + return Err(ParseError::new_syntax( + t.name.span, + format!("duplicate item named `{}`", t.name.name), + )); + } + let prev = decl_list_ns.insert(t.name.name, ()); + assert!(prev.is_none()); + // Package-scope types are not part of the iface/world + // dependency order graph. + let prev = names.insert(t.name.name, item); + assert!(prev.is_none()); + self.pending_package_type_names.insert(t.name.name); + } // These are processed down below. ast::AstItem::Use(_) => {} @@ -416,6 +496,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())), + ast::AstItem::Type(t) => (&t.name, ItemSource::Local(t.name.clone())), ast::AstItem::Package(_) => unreachable!(), }; if decl_list_ns.insert(name.name, (name.span, src)).is_some() { @@ -443,10 +524,16 @@ impl<'a> Resolver<'a> { match decl_list_ns.get(used_name.name) { Some((_, ItemSource::Foreign)) => return Ok(()), Some((_, ItemSource::Local(id))) => { + if matches!(names.get(id.name), Some(ast::AstItem::Type(_))) { + return Ok(()); + } order[iface.name].push(id.clone()); } None => match package_items.get(used_name.name) { Some(_) => { + if matches!(names.get(used_name.name), Some(ast::AstItem::Type(_))) { + return Ok(()); + } order[iface.name].push(used_name.clone()); } None => { @@ -488,11 +575,23 @@ impl<'a> Resolver<'a> { assert!(prev.is_none()); world_id_order.push(id); } - ast::AstItem::Use(_) | ast::AstItem::Package(_) => unreachable!(), + ast::AstItem::Type(_) + | ast::AstItem::Use(_) + | ast::AstItem::Package(_) => unreachable!(), }; } + Ok((iface_id_order, world_id_order, ids)) + } + + fn finish_ast_items( + &mut self, + decl_lists: &[ast::DeclList<'a>], + ids: &IndexMap<&'a str, AstItem>, + ) -> ParseResult<()> { + let self_pkg = self.package_name.as_ref().map(|(n, _)| n.clone()); for decl_list in decl_lists { let mut items = IndexMap::default(); + let mut type_items = IndexMap::default(); for item in decl_list.items.iter() { let (name, ast_item) = match item { ast::AstItem::Use(u) => { @@ -502,21 +601,61 @@ impl<'a> Resolver<'a> { format!("attributes not allowed on top-level use"), )); } - let name = u.as_.as_ref().unwrap_or(u.item.name()); + let alias = u.as_.as_ref().unwrap_or(u.item.name()); let item = match &u.item { - ast::UsePath::Id(name) => *ids.get(name.name).ok_or_else(|| { - ParseError::from(ParseErrorKind::ItemNotFound { - span: name.span, - name: name.name.to_string(), - kind: "interface or world".to_owned(), - hint: None, - }) - })?, + ast::UsePath::Id(name) => { + match ids.get(name.name) { + Some(item) => *item, + None if self.pending_package_type_names.contains(name.name) => { + // Package-scope type; registered after + // resolve_package_types. + continue; + } + None => { + return Err(ParseError::from(ParseErrorKind::ItemNotFound { + span: name.span, + name: name.name.to_string(), + kind: "interface, world, or type".to_owned(), + hint: None, + })); + } + } + } ast::UsePath::Package { id, name } => { - self.foreign_deps[&id.package_name()][name.name].0 + if self_pkg.as_ref() == Some(&id.package_name()) { + match ids.get(name.name) { + Some(item) => *item, + None if self + .pending_package_type_names + .contains(name.name) => + { + continue; + } + None => { + return Err(ParseError::from( + ParseErrorKind::ItemNotFound { + span: name.span, + name: name.name.to_string(), + kind: "interface, world, or type".to_owned(), + hint: None, + }, + )); + } + } + } else { + let pkg = id.package_name(); + let key = (pkg.clone(), name.name.to_string()); + if let Some((_iface, ty)) = self.foreign_unknown.get(&key) { + type_items.insert(alias.name, *ty); + } + self.foreign_deps[&pkg][name.name].0 + } } }; - (name.name, item) + if let AstItem::Type(ty) = item { + type_items.insert(alias.name, ty); + } + (alias.name, item) } ast::AstItem::Interface(i) => { let iface_item = ids[i.name.name]; @@ -528,6 +667,7 @@ impl<'a> Resolver<'a> { assert!(matches!(world_item, AstItem::World(_))); (w.name.name, world_item) } + ast::AstItem::Type(_) => continue, ast::AstItem::Package(_) => unreachable!(), }; let prev = items.insert(name, ast_item); @@ -541,8 +681,144 @@ impl<'a> Resolver<'a> { } } self.ast_items.push(items); + self.ast_type_items.push(type_items); } - Ok((iface_id_order, world_id_order)) + Ok(()) + } + + /// Resolve all package-scope typedefs into `self.types` / `self.package_types`. + fn resolve_package_types( + &mut self, + decl_lists: &[ast::DeclList<'a>], + ids: &mut IndexMap<&'a str, AstItem>, + ) -> ParseResult<()> { + let mut type_defs = IndexMap::default(); + let mut type_files = IndexMap::default(); + for (i, decl_list) in decl_lists.iter().enumerate() { + for item in decl_list.items.iter() { + let ast::AstItem::Type(t) = item else { + continue; + }; + let prev = type_defs.insert(t.name.name, t); + assert!(prev.is_none()); + type_files.insert(t.name.name, i); + } + } + + let mut type_deps = IndexMap::default(); + for (name, def) in type_defs.iter() { + let mut deps = Vec::new(); + collect_deps(&def.ty, &mut deps); + // Only other package-scope types participate in topo ordering. + // Same-file foreign `use` aliases are already allocated stubs and + // are resolved via `ast_type_items` with `cur_ast_index` set below. + deps.retain(|d| type_defs.contains_key(d.name)); + type_deps.insert(*name, deps); + } + + let order = toposort("type", &type_deps)?; + for name in order { + // Allow same-file toplevel `use` aliases (e.g. foreign package + // types). Cross-file uses remain invisible because `use` is + // file-scoped. + self.cur_ast_index = type_files[name]; + let def = type_defs[name]; + let docs = self.docs(&def.docs); + let stability = self.stability(&def.attributes)?; + let external_id = self.external_id(&def.attributes)?; + let kind = self.resolve_type_def(&def.ty, &stability)?; + let id = self.types.alloc(TypeDef { + docs, + stability, + kind, + name: Some(def.name.name.to_string()), + owner: TypeOwner::None, + span: def.name.span, + external_id, + }); + let prev = self.package_types.insert(def.name.name, id); + assert!(prev.is_none()); + let prev = ids.insert(def.name.name, AstItem::Type(id)); + assert!(prev.is_none()); + } + Ok(()) + } + + /// After package-scope types are allocated, register them in package and + /// per-file namespaces, and finish deferred top-level `use` aliases. + fn register_package_types_in_ns( + &mut self, + decl_lists: &[ast::DeclList<'a>], + ids: &IndexMap<&'a str, AstItem>, + ) -> ParseResult<()> { + let self_pkg = self.package_name.as_ref().map(|(n, _)| n.clone()); + for (i, decl_list) in decl_lists.iter().enumerate() { + for item in decl_list.items.iter() { + match item { + ast::AstItem::Type(t) => { + let type_item = ids[t.name.name]; + assert!(matches!(type_item, AstItem::Type(_))); + if let AstItem::Type(ty) = type_item { + self.ast_type_items[i].insert(t.name.name, ty); + } + let prev = self.ast_items[i].insert(t.name.name, type_item); + assert!(prev.is_none()); + let prev = self.package_items.insert(t.name.name, type_item); + assert!(prev.is_none()); + } + ast::AstItem::Use(u) => { + let alias = u.as_.as_ref().unwrap_or(u.item.name()); + if self.ast_items[i].contains_key(alias.name) { + continue; + } + let item = match &u.item { + ast::UsePath::Id(name) => match ids.get(name.name) { + Some(AstItem::Type(ty)) => { + self.ast_type_items[i].insert(alias.name, *ty); + AstItem::Type(*ty) + } + Some(_) => continue, + None => { + return Err(ParseError::from(ParseErrorKind::ItemNotFound { + span: name.span, + name: name.name.to_string(), + kind: "interface, world, or type".to_owned(), + hint: None, + })); + } + }, + ast::UsePath::Package { id, name } => { + if self_pkg.as_ref() != Some(&id.package_name()) { + continue; + } + match ids.get(name.name) { + Some(AstItem::Type(ty)) => { + self.ast_type_items[i].insert(alias.name, *ty); + AstItem::Type(*ty) + } + Some(_) => continue, + None => { + return Err(ParseError::from( + ParseErrorKind::ItemNotFound { + span: name.span, + name: name.name.to_string(), + kind: "interface, world, or type".to_owned(), + hint: None, + }, + )); + } + } + } + }; + let prev = self.ast_items[i].insert(alias.name, item); + assert!(prev.is_none()); + } + _ => {} + } + } + } + self.pending_package_type_names.clear(); + Ok(()) } /// Generate a `Type::Unknown` entry for all types imported from foreign @@ -850,8 +1126,10 @@ impl<'a> Resolver<'a> { for field in fields { match field { ast::InterfaceItem::Func(f) => { - self.define_interface_name(&f.name, TypeOrItem::Item("function"))?; - funcs.push(self.resolve_function( + // Resolve the signature before reserving the function name so + // package-scope (and local) types with the same name remain + // visible to params/results. + let func = self.resolve_function( &f.docs, &f.attributes, &f.name.name, @@ -862,7 +1140,9 @@ impl<'a> Resolver<'a> { } else { FunctionKind::Freestanding }, - )?); + )?; + self.define_interface_name(&f.name, TypeOrItem::Item("function"))?; + funcs.push(func); } ast::InterfaceItem::Use(_) => {} ast::InterfaceItem::TypeDef(ast::TypeDef { @@ -939,6 +1219,32 @@ impl<'a> Resolver<'a> { } } } + + // Package-scope and file-level type aliases are visible without being + // in `type_lookup`. Add phantom entries so toposort does not fail when + // local defs reference them. + let mut phantoms = Vec::new(); + for deps in type_deps.values() { + for dep in deps { + if type_deps.contains_key(dep.name) { + continue; + } + let is_package = self.package_types.contains_key(dep.name); + let is_file = self + .ast_type_items + .get(self.cur_ast_index) + .map(|m| m.contains_key(dep.name)) + .unwrap_or(false); + if is_package || is_file { + phantoms.push(dep.name); + } + } + } + for name in phantoms { + type_deps.insert(name, Vec::new()); + type_defs.insert(name, None); + } + let order = toposort("type", &type_deps).map_err(attach_old_float_type_context)?; for ty in order { let def = match type_defs.swap_remove(&ty).unwrap() { @@ -1124,21 +1430,44 @@ impl<'a> Resolver<'a> { .or_else(|| self.package_items.get(id.name)); match item { Some(item) => Ok((*item, id.name.into(), id.span)), - None => { - return Err(ParseError::from(ParseErrorKind::ItemNotFound { - span: id.span, - name: id.name.to_string(), - kind: "interface or world".to_owned(), - hint: None, - })); + None if self.pending_package_type_names.contains(id.name) => { + Err(ParseError::new_syntax( + id.span, + format!( + "name `{}` is defined as a type, not an interface", + id.name + ), + )) } + None => Err(ParseError::from(ParseErrorKind::ItemNotFound { + span: id.span, + name: id.name.to_string(), + kind: "interface or world".to_owned(), + hint: None, + })), } } - ast::UsePath::Package { id, name } => Ok(( - self.foreign_deps[&id.package_name()][name.name].0, - name.name.into(), - name.span, - )), + ast::UsePath::Package { id, name } => { + let pkg = id.package_name(); + if let Some((item, _)) = self + .foreign_deps + .get(&pkg) + .and_then(|deps| deps.get(name.name)) + { + return Ok((*item, name.name.into(), name.span)); + } + if self.package_name.as_ref().map(|(n, _)| n) == Some(&pkg) { + if let Some(item) = self.package_items.get(name.name) { + return Ok((*item, name.name.into(), name.span)); + } + } + Err(ParseError::from(ParseErrorKind::ItemNotFound { + span: name.span, + name: name.name.to_string(), + kind: "interface, world, or type".to_owned(), + hint: None, + })) + } } } @@ -1156,6 +1485,12 @@ impl<'a> Resolver<'a> { format!("name `{name}` is defined as a world, not an interface"), )); } + AstItem::Type(_) => { + return Err(ParseError::new_syntax( + span, + format!("name `{name}` is defined as a type, not an interface"), + )); + } } } @@ -1173,6 +1508,12 @@ impl<'a> Resolver<'a> { format!("name `{name}` is defined as an interface, not a world"), )); } + AstItem::Type(_) => { + return Err(ParseError::new_syntax( + span, + format!("name `{name}` is defined as a type, not a world"), + )); + } } } @@ -1375,6 +1716,16 @@ impl<'a> Resolver<'a> { )); } None => { + if let Some(id) = self.package_types.get(name.name) { + return Ok(*id); + } + if let Some(id) = self + .ast_type_items + .get(self.cur_ast_index) + .and_then(|m| m.get(name.name)) + { + return Ok(*id); + } return Err(ParseError::from(ParseErrorKind::ItemNotFound { span: name.span, name: name.name.to_string(), diff --git a/crates/wit-parser/src/decoding.rs b/crates/wit-parser/src/decoding.rs index 6b6f0498e0..82e3325171 100644 --- a/crates/wit-parser/src/decoding.rs +++ b/crates/wit-parser/src/decoding.rs @@ -145,8 +145,8 @@ impl ComponentInfo { } fn is_wit_package(&self) -> Option { - // all wit package exports must be component types, and there must be at - // least one + // all wit package exports must be types (component or defined), and + // there must be at least one if self.externs.is_empty() { return None; } @@ -157,9 +157,10 @@ impl ComponentInfo { _ => return false, }; match export.ty { - ComponentEntityType::Type { created, .. } => { - matches!(created, ComponentAnyTypeId::Component(_)) - } + ComponentEntityType::Type { created, .. } => matches!( + created, + ComponentAnyTypeId::Component(_) | ComponentAnyTypeId::Defined(_) + ), _ => false, } }) { @@ -214,61 +215,87 @@ impl ComponentInfo { let mut interfaces = IndexMap::default(); let mut worlds = IndexMap::default(); + let mut types = IndexMap::default(); let mut fields = PackageFields { interfaces: &mut interfaces, worlds: &mut worlds, + types: &mut types, }; - for (_, item) in self.externs.iter() { + for (export_name, item) in self.externs.iter() { let export = match item { Extern::Export(e) => e, _ => unreachable!(), }; - let component = match export.ty { + match export.ty { + ComponentEntityType::Type { + created: ComponentAnyTypeId::Defined(_), + .. + } => { + let name = { + let parsed = ComponentName::new(export_name, 0) + .with_context(|| format!("invalid export name `{export_name}`"))?; + match parsed.kind() { + ComponentNameKind::Label(label) => label.to_string(), + _ => bail!( + "package-scope type export `{export_name}` must be a kebab name" + ), + } + }; + let id = + decoder.register_type_export(&name, export, TypeOwner::None)?; + let prev = fields.types.insert(name, id); + if prev.is_some() { + bail!("duplicate package-scope type export"); + } + } ComponentEntityType::Type { created: ComponentAnyTypeId::Component(id), .. - } => &self.types[id], - _ => unreachable!(), - }; - - // The single export of this component will determine if it's a world or an interface: - // worlds export a component, while interfaces export an instance. - if component.exports.len() != 1 { - bail!( - "Expected a single export, but found {} instead", - component.exports.len() - ); - } + } => { + let component = &self.types[id]; + + // The single export of this component will determine if it's a world or an interface: + // worlds export a component, while interfaces export an instance. + if component.exports.len() != 1 { + bail!( + "Expected a single export, but found {} instead", + component.exports.len() + ); + } - let name = component.exports.keys().nth(0).unwrap(); + let name = component.exports.keys().nth(0).unwrap(); - let item = &component.exports[name]; - let name = match item.ty { - ComponentEntityType::Component(_) => { - let package_name = decoder.decode_world(name.as_str(), item, &mut fields)?; - package_name - } - ComponentEntityType::Instance(_) => { - let package_name = decoder.decode_interface( - name.as_str(), - &component.imports, - item, - &mut fields, - )?; - package_name + let item = &component.exports[name]; + let name = match item.ty { + ComponentEntityType::Component(_) => { + let package_name = + decoder.decode_world(name.as_str(), item, &mut fields)?; + package_name + } + ComponentEntityType::Instance(_) => { + let package_name = decoder.decode_interface( + name.as_str(), + &component.imports, + item, + &mut fields, + )?; + package_name + } + _ => unreachable!(), + }; + + if let Some(pkg_name) = pkg_name.as_ref() { + // TODO: when we have fully switched to the v2 format, we should switch to parsing + // multiple wit documents instead of bailing. + if pkg_name != &name { + bail!("item defined with mismatched package name") + } + } else { + pkg_name.replace(name); + } } _ => unreachable!(), - }; - - if let Some(pkg_name) = pkg_name.as_ref() { - // TODO: when we have fully switched to the v2 format, we should switch to parsing - // multiple wit documents instead of bailing. - if pkg_name != &name { - bail!("item defined with mismatched package name") - } - } else { - pkg_name.replace(name); } } @@ -278,7 +305,14 @@ impl ComponentInfo { docs: Docs::default(), interfaces, worlds, + types, } + } else if !types.is_empty() { + bail!( + "WIT package with only package-scope types (no interfaces or worlds) \ + cannot be decoded; add an interface or world so the package name can \ + be recovered from the binary format" + ); } else { bail!("no exported component type found"); }; @@ -319,11 +353,13 @@ impl ComponentInfo { docs: Default::default(), worlds: [(world_name.to_string(), world)].into_iter().collect(), interfaces: Default::default(), + types: Default::default(), }; let mut fields = PackageFields { worlds: &mut package.worlds, interfaces: &mut package.interfaces, + types: &mut package.types, }; for (name, item) in self.externs.iter() { @@ -467,6 +503,7 @@ pub fn decode_world(wasm: &[u8]) -> Result<(Resolve, WorldId)> { let mut decoder = WitPackageDecoder::new(types); let mut interfaces = IndexMap::default(); let mut worlds = IndexMap::default(); + let mut pkg_types = IndexMap::default(); let ty = &types[world]; assert_eq!(ty.imports.len(), 0); assert_eq!(ty.exports.len(), 1); @@ -477,12 +514,14 @@ pub fn decode_world(wasm: &[u8]) -> Result<(Resolve, WorldId)> { &mut PackageFields { interfaces: &mut interfaces, worlds: &mut worlds, + types: &mut pkg_types, }, )?; let (resolve, pkg) = decoder.finish(Package { name, interfaces, worlds, + types: pkg_types, docs: Default::default(), }); // The package decoded here should only have a single world so extract that @@ -494,6 +533,7 @@ pub fn decode_world(wasm: &[u8]) -> Result<(Resolve, WorldId)> { struct PackageFields<'a> { interfaces: &'a mut IndexMap, worlds: &'a mut IndexMap, + types: &'a mut IndexMap, } struct WitPackageDecoder<'a> { @@ -558,11 +598,13 @@ impl WitPackageDecoder<'_> { docs: Default::default(), interfaces: Default::default(), worlds: Default::default(), + types: Default::default(), }; let mut fields = PackageFields { interfaces: &mut package.interfaces, worlds: &mut package.worlds, + types: &mut package.types, }; for (name, ty) in ty.exports.iter() { @@ -576,7 +618,17 @@ impl WitPackageDecoder<'_> { self.register_world(name.as_str(), ty, &mut fields) .with_context(|| format!("failed to process export `{name}`"))?; } - _ => bail!("component export `{name}` is not an instance or component"), + ComponentEntityType::Type { + created: ComponentAnyTypeId::Defined(_), + .. + } => { + let id = self.register_type_export(name.as_str(), ty, TypeOwner::None)?; + let prev = fields.types.insert(name.to_string(), id); + if prev.is_some() { + bail!("duplicate package-scope type export `{name}`"); + } + } + _ => bail!("component export `{name}` is not an instance, component, or type"), } } Ok(package) @@ -893,6 +945,7 @@ impl WitPackageDecoder<'_> { docs: Default::default(), interfaces: Default::default(), worlds: Default::default(), + types: Default::default(), }); let interface = *package .interfaces @@ -1505,6 +1558,7 @@ impl WitPackageDecoder<'_> { name, interfaces, worlds, + types, docs, } = package; @@ -1523,6 +1577,7 @@ impl WitPackageDecoder<'_> { name: name.clone(), interfaces: Default::default(), worlds: Default::default(), + types: Default::default(), docs, }); let prev = self.resolve.package_names.insert(name, id); @@ -1553,9 +1608,133 @@ impl WitPackageDecoder<'_> { } } + for (name, id) in types { + let prev = self.resolve.packages[pkg].types.insert(name, id); + assert!(prev.is_none()); + self.resolve.types[id].owner = TypeOwner::Package(pkg); + } + + // Encoding inlines package-scope types into interface/world instances + // and exports them for validation. Drop those duplicates so round-trip + // WIT keeps package ownership (bare names, no invented interface decls). + self.coalesce_inlined_package_types(pkg); + pkg } + /// Rewrite interface/world references that duplicate package-scope types. + fn coalesce_inlined_package_types(&mut self, pkg: PackageId) { + let package_types = self.resolve.packages[pkg].types.clone(); + if package_types.is_empty() { + return; + } + + let mut remap = HashMap::new(); + let iface_ids: Vec<_> = self.resolve.packages[pkg] + .interfaces + .values() + .copied() + .collect(); + for iface_id in iface_ids { + self.coalesce_package_types_in_interface(iface_id, &package_types, &mut remap); + } + + let world_ids: Vec<_> = self.resolve.packages[pkg] + .worlds + .values() + .copied() + .collect(); + for world_id in world_ids { + let mut remove = Vec::new(); + let mut inline_ifaces = Vec::new(); + for (key, item) in self.resolve.worlds[world_id].imports.iter() { + match (key, item) { + (WorldKey::Name(name), WorldItem::Type { id, .. }) => { + if let Some(&pkg_ty) = package_types.get(name) { + if *id != pkg_ty { + remap.insert(*id, pkg_ty); + remove.push(key.clone()); + } + } + } + (_, WorldItem::Interface { id, .. }) => { + inline_ifaces.push(*id); + } + _ => {} + } + } + for (_key, item) in self.resolve.worlds[world_id].exports.iter() { + if let WorldItem::Interface { id, .. } = item { + inline_ifaces.push(*id); + } + } + for key in remove { + self.resolve.worlds[world_id].imports.swap_remove(&key); + } + for iface_id in inline_ifaces { + // Named package interfaces are handled above; still coalesce + // anonymous ifaces reachable from worlds. + if self.resolve.interfaces[iface_id].name.is_none() { + self.coalesce_package_types_in_interface( + iface_id, + &package_types, + &mut remap, + ); + } + } + } + + if remap.is_empty() { + return; + } + + for (_id, ty) in self.resolve.types.iter_mut() { + rewrite_type_ids_in_kind(&mut ty.kind, &remap); + } + for (_id, iface) in self.resolve.interfaces.iter_mut() { + for func in iface.functions.values_mut() { + rewrite_type_ids_in_func(func, &remap); + } + } + for (_id, world) in self.resolve.worlds.iter_mut() { + for item in world + .imports + .values_mut() + .chain(world.exports.values_mut()) + { + match item { + WorldItem::Function(f) => rewrite_type_ids_in_func(f, &remap), + WorldItem::Type { id, .. } => { + if let Some(&new) = remap.get(id) { + *id = new; + } + } + WorldItem::Interface { .. } => {} + } + } + } + } + + fn coalesce_package_types_in_interface( + &mut self, + iface_id: InterfaceId, + package_types: &IndexMap, + remap: &mut HashMap, + ) { + let mut remove = Vec::new(); + for (name, &ty_id) in self.resolve.interfaces[iface_id].types.iter() { + if let Some(&pkg_ty) = package_types.get(name) { + if ty_id != pkg_ty { + remap.insert(ty_id, pkg_ty); + remove.push(name.clone()); + } + } + } + for name in remove { + self.resolve.interfaces[iface_id].types.swap_remove(&name); + } + } + fn visit_package(&self, idx: usize, order: &mut IndexSet) { if order.contains(&idx) { return; @@ -1875,3 +2054,77 @@ impl InterfaceNameExt for wasmparser::names::InterfaceName<'_> { }) } } + +fn rewrite_type_id(id: &mut TypeId, remap: &HashMap) { + if let Some(new) = remap.get(id) { + *id = *new; + } +} + +fn rewrite_type(ty: &mut Type, remap: &HashMap) { + if let Type::Id(id) = ty { + rewrite_type_id(id, remap); + } +} + +fn rewrite_type_ids_in_kind(kind: &mut TypeDefKind, remap: &HashMap) { + match kind { + TypeDefKind::Record(r) => { + for field in r.fields.iter_mut() { + rewrite_type(&mut field.ty, remap); + } + } + TypeDefKind::Tuple(t) => { + for ty in t.types.iter_mut() { + rewrite_type(ty, remap); + } + } + TypeDefKind::Variant(v) => { + for case in v.cases.iter_mut() { + if let Some(ty) = &mut case.ty { + rewrite_type(ty, remap); + } + } + } + TypeDefKind::Option(t) | TypeDefKind::List(t) | TypeDefKind::Type(t) => { + rewrite_type(t, remap); + } + TypeDefKind::FixedLengthList(t, _) => rewrite_type(t, remap), + TypeDefKind::Map(k, v) => { + rewrite_type(k, remap); + rewrite_type(v, remap); + } + TypeDefKind::Result(r) => { + if let Some(ty) = &mut r.ok { + rewrite_type(ty, remap); + } + if let Some(ty) = &mut r.err { + rewrite_type(ty, remap); + } + } + TypeDefKind::Future(t) | TypeDefKind::Stream(t) => { + if let Some(ty) = t { + rewrite_type(ty, remap); + } + } + TypeDefKind::Handle(Handle::Own(id) | Handle::Borrow(id)) => { + rewrite_type_id(id, remap); + } + TypeDefKind::Flags(_) + | TypeDefKind::Enum(_) + | TypeDefKind::Resource + | TypeDefKind::Unknown => {} + } +} + +fn rewrite_type_ids_in_func(func: &mut Function, remap: &HashMap) { + for param in func.params.iter_mut() { + rewrite_type(&mut param.ty, remap); + } + if let Some(ty) = &mut func.result { + rewrite_type(ty, remap); + } + if let Some(id) = func.kind.resource_mut() { + rewrite_type_id(id, remap); + } +} diff --git a/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs index d1aece5a81..1ce7bdb598 100644 --- a/crates/wit-parser/src/lib.rs +++ b/crates/wit-parser/src/lib.rs @@ -163,6 +163,18 @@ pub struct UnresolvedPackage { /// Doc comments for this package. pub docs: Docs, + /// Named types declared at package scope, keyed by kebab-name. + /// + /// These types temporarily use [`TypeOwner::None`] until they are merged + /// into a [`Resolve`], at which point their owner becomes + /// [`TypeOwner::Package`]. + pub package_types: IndexMap, + + /// Foreign toplevel `use ns:pkg/name` entries whose target may be either an + /// interface or a package-scope type. Each entry records the dual stubs + /// allocated before the dependency is known. + pub foreign_unknown: IndexMap<(PackageName, String), (InterfaceId, TypeId)>, + #[cfg_attr(not(feature = "std"), allow(dead_code))] package_name_span: Span, unknown_type_spans: Vec, @@ -225,6 +237,9 @@ pub enum AstItem { Interface(InterfaceId), #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))] World(WorldId), + /// A package-scope type declaration. + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))] + Type(TypeId), } /// A structure used to keep track of the name of a package, containing optional @@ -730,6 +745,9 @@ pub enum TypeOwner { /// This type was defined within an `interface` block. #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))] Interface(InterfaceId), + /// This type was defined at package scope. + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))] + Package(PackageId), /// This type wasn't inherently defined anywhere, such as a `list`, which /// doesn't need an owner. #[cfg_attr(feature = "serde", serde(untagged, serialize_with = "serialize_none"))] diff --git a/crates/wit-parser/src/metadata.rs b/crates/wit-parser/src/metadata.rs index f1972f7a3a..f73c1f5d53 100644 --- a/crates/wit-parser/src/metadata.rs +++ b/crates/wit-parser/src/metadata.rs @@ -68,6 +68,11 @@ pub struct PackageMetadata { serde(default, skip_serializing_if = "StringMap::is_empty") )] interfaces: StringMap, + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "StringMap::is_empty") + )] + types: StringMap, } impl PackageMetadata { @@ -89,11 +94,18 @@ impl PackageMetadata { .map(|(name, id)| (name.to_string(), InterfaceMetadata::extract(resolve, *id))) .filter(|(_, item)| !item.is_empty()) .collect(); + let types = package + .types + .iter() + .map(|(name, id)| (name.to_string(), TypeMetadata::extract(resolve, *id))) + .filter(|(_, item)| !item.is_empty()) + .collect(); Self { docs: package.docs.contents.as_deref().map(Into::into), worlds, interfaces, + types, } } @@ -113,6 +125,12 @@ impl PackageMetadata { }; docs.inject(resolve, id)?; } + for (name, docs) in &self.types { + let Some(&id) = resolve.packages[package].types.get(name) else { + bail!("missing package type {name:?}"); + }; + docs.inject(resolve, id)?; + } if let Some(docs) = &self.docs { resolve.packages[package].docs.contents = Some(docs.to_string()); } diff --git a/crates/wit-parser/src/resolve/clone.rs b/crates/wit-parser/src/resolve/clone.rs index 9e2df3d2fe..11e1b92a6e 100644 --- a/crates/wit-parser/src/resolve/clone.rs +++ b/crates/wit-parser/src/resolve/clone.rs @@ -109,6 +109,17 @@ impl<'a> Cloner<'a> { } fn type_id(&mut self, ty: &mut TypeId) { + let owner = self.resolve.types[*ty].owner; + // Package-scope types (and other types not owned by the world/interface + // being cloned) are shared by reference across includes. Only types + // owned by `prev_owner` (or anonymous `None` types) are deep-cloned. + if owner != TypeOwner::None && owner != self.prev_owner { + if let Some(new_id) = self.maps.types.get(ty) { + *ty = *new_id; + } + return; + } + if !self.maps.types.contains_key(ty) { let mut new = self.resolve.types[*ty].clone(); self.type_def(&mut new); @@ -226,6 +237,7 @@ impl<'a> Cloner<'a> { new.package = Some(self.new_package.unwrap_or_else(|| match self.new_owner { TypeOwner::Interface(id) => self.resolve.interfaces[id].package.unwrap(), TypeOwner::World(id) => self.resolve.worlds[id].package.unwrap(), + TypeOwner::Package(id) => id, TypeOwner::None => unreachable!(), })); new.clone_of = Some(old_id); diff --git a/crates/wit-parser/src/resolve/mod.rs b/crates/wit-parser/src/resolve/mod.rs index 388e2767b7..e21c9e5b48 100644 --- a/crates/wit-parser/src/resolve/mod.rs +++ b/crates/wit-parser/src/resolve/mod.rs @@ -108,9 +108,9 @@ pub struct Resolve { /// A WIT package within a `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. +/// A package is a collection of interfaces, worlds, and package-scope types. +/// Packages additionally have a unique identifier that affects generated +/// components and uniquely identifiers this particular package. #[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(Serialize))] pub struct Package { @@ -129,6 +129,13 @@ pub struct Package { /// All worlds contained in this package, keyed by the world's name. #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id_map"))] pub worlds: IndexMap, + + /// Types declared at package scope, keyed by the type's kebab-name. + /// + /// These share the same top-level name space as [`Self::interfaces`] and + /// [`Self::worlds`]. + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id_map"))] + pub types: IndexMap, } pub type PackageId = Id; @@ -522,6 +529,7 @@ impl Resolve { world_map, interfaces_to_add, worlds_to_add, + types_to_add, .. } = map; @@ -700,6 +708,9 @@ impl Resolve { for (_, id) in pkg.worlds.iter_mut() { *id = remap.map_world(*id, Default::default())?; } + for (_, id) in pkg.types.iter_mut() { + *id = remap.map_type(*id, Default::default())?; + } self.packages.alloc(pkg) } }; @@ -741,6 +752,7 @@ impl Resolve { match &mut self.types[id].owner { TypeOwner::Interface(id) => *id = remap.map_interface(*id, Default::default())?, TypeOwner::World(id) => *id = remap.map_world(*id, Default::default())?, + TypeOwner::Package(id) => *id = remap.packages[id.index()], TypeOwner::None => {} } } @@ -761,6 +773,12 @@ impl Resolve { .insert(name, remap.map_world(world, Default::default())?); assert!(prev.is_none()); } + for (name, pkg, ty) in types_to_add { + let prev = self.packages[pkg] + .types + .insert(name, remap.map_type(ty, Default::default())?); + assert!(prev.is_none()); + } log::trace!("now have {} packages", self.packages.len()); @@ -1783,6 +1801,11 @@ impl Resolve { assert!(self.worlds.get(id).is_some()); assert!(world_types[id.index()].contains(&ty_id)); } + TypeOwner::Package(id) => { + assert!(self.packages.get(id).is_some()); + let name = ty.name.as_ref().expect("package type must be named"); + assert_eq!(self.packages[id].types.get(name), Some(&ty_id)); + } TypeOwner::None => {} } } @@ -1938,6 +1961,11 @@ impl Resolve { visitor.visit_type_def(self, ty); for ty in visitor.1 { let ty = &self.types[ty]; + // Package-scope types are visible without being imported into the + // world (they are not world-owned names). + if matches!(ty.owner, TypeOwner::Package(_)) { + continue; + } let Some(name) = ty.name.clone() else { continue; }; @@ -3488,6 +3516,7 @@ impl Remap { docs: unresolved.docs.clone(), interfaces: Default::default(), worlds: Default::default(), + types: Default::default(), }); assert!( !resolve.package_names.contains_key(&unresolved.name), @@ -3500,6 +3529,7 @@ impl Remap { let foreign_types = self.types.len(); let foreign_interfaces = self.interfaces.len(); let foreign_worlds = self.worlds.len(); + let package_types = unresolved.package_types.clone(); // Copy over all types first, updating any intra-type references. Note // that types are sorted topologically which means this iteration @@ -3514,6 +3544,11 @@ impl Remap { } self.update_typedef(resolve, &mut ty, span)?; + // Package-scope types are tracked on the unresolved package and get + // their owner rewritten here once PackageId is known. + if package_types.values().any(|tid| *tid == id) { + ty.owner = TypeOwner::Package(pkgid); + } let new_id = resolve.types.alloc(ty); assert_eq!(self.types.len(), id.index()); @@ -3567,7 +3602,7 @@ impl Remap { TypeOwner::Interface(iface_id) => { *iface_id = self.map_interface_for_type(*iface_id, span)?; } - TypeOwner::World(_) | TypeOwner::None => {} + TypeOwner::World(_) | TypeOwner::Package(_) | TypeOwner::None => {} } } @@ -3602,7 +3637,7 @@ impl Remap { TypeOwner::World(world_id) => { *world_id = self.map_world_for_type(*world_id, span)?; } - TypeOwner::Interface(_) | TypeOwner::None => {} + TypeOwner::Interface(_) | TypeOwner::Package(_) | TypeOwner::None => {} } } @@ -3657,6 +3692,13 @@ impl Remap { .insert(world.name.clone(), id); assert!(prev.is_none()); } + for (name, old_id) in package_types { + // Note that errors here mean the type was filtered by stability. + if let Ok(new_id) = self.map_type(old_id, Default::default()) { + let prev = resolve.packages[pkgid].types.insert(name, new_id); + assert!(prev.is_none()); + } + } Ok(pkgid) } @@ -3687,6 +3729,7 @@ impl Remap { ); assert!(prev.is_none()); } + AstItem::Type(_) => {} } } } @@ -3781,15 +3824,65 @@ impl Remap { continue; } - let iface_id = pkg.interfaces.get(interface).copied().ok_or_else(|| { - ResolveError::from(ResolveErrorKind::InterfaceNotFound { - span: iface_span, - requested: interface.to_string(), - package: pkg.name.clone(), - }) - })?; - assert_eq!(self.interfaces.len(), unresolved_iface_id.index()); - self.interfaces.push(Some(iface_id)); + let key = (pkg_name.clone(), interface.clone()); + let is_unknown = unresolved.foreign_unknown.contains_key(&key); + + let iface_id = pkg.interfaces.get(interface).copied(); + let type_id = pkg.types.get(interface).copied(); + let world_id = pkg.worlds.get(interface).copied(); + + if is_unknown { + match (iface_id, type_id, world_id) { + (Some(iface_id), None, None) => { + assert_eq!(self.interfaces.len(), unresolved_iface_id.index()); + self.interfaces.push(Some(iface_id)); + // Type stub abandoned in process_foreign_types. + } + (None, Some(_), None) => { + // Interface stub abandoned; type mapped in process_foreign_types. + self.interfaces.push(None); + } + (None, None, Some(_)) => { + return Err(ResolveError::new_semantic( + iface_span, + format!( + "`{interface}` is a world; top-level use expects an interface or type" + ), + )); + } + (None, None, None) => { + return Err(ResolveError::new_semantic( + iface_span, + format!( + "interface or type `{interface}` not found in package `{}`", + pkg.name + ), + )); + } + _ => unreachable!("provider package enforces a shared top-level name space"), + } + } else { + let iface_id = match iface_id { + Some(id) => id, + None if type_id.is_some() => { + return Err(ResolveError::new_semantic( + iface_span, + format!( + "name `{interface}` is defined as a type, not an interface" + ), + )); + } + None => { + return Err(ResolveError::from(ResolveErrorKind::InterfaceNotFound { + span: iface_span, + requested: interface.to_string(), + package: pkg.name.clone(), + })); + } + }; + assert_eq!(self.interfaces.len(), unresolved_iface_id.index()); + self.interfaces.push(Some(iface_id)); + } } for (id, _) in unresolved.interfaces.iter().skip(self.interfaces.len()) { assert!( @@ -3883,24 +3976,56 @@ impl Remap { continue; } - let unresolved_iface_id = match unresolved_ty.owner { - TypeOwner::Interface(id) => id, - _ => unreachable!(), - }; - let iface_id = self.map_interface(unresolved_iface_id, Default::default())?; - let name = unresolved_ty.name.as_ref().unwrap(); - let span = unresolved.unknown_type_spans[unresolved_type_id.index()]; - let type_id = *resolve.interfaces[iface_id] - .types - .get(name) - .ok_or_else(|| { - ResolveError::new_semantic( - span, - format!("type `{name}` not defined in interface"), - ) - })?; - assert_eq!(self.types.len(), unresolved_type_id.index()); - self.types.push(Some(type_id)); + match unresolved_ty.owner { + TypeOwner::Interface(unresolved_iface_id) => { + let iface_id = self.map_interface(unresolved_iface_id, Default::default())?; + let name = unresolved_ty.name.as_ref().unwrap(); + let span = unresolved.unknown_type_spans[unresolved_type_id.index()]; + let type_id = *resolve.interfaces[iface_id] + .types + .get(name) + .ok_or_else(|| { + ResolveError::new_semantic( + span, + format!("type `{name}` not defined in interface"), + ) + })?; + assert_eq!(self.types.len(), unresolved_type_id.index()); + self.types.push(Some(type_id)); + } + TypeOwner::None => { + // Dual-stub Unknown types from toplevel `use` of a package-scope type. + let name = unresolved_ty.name.as_ref().unwrap(); + let span = unresolved.unknown_type_spans[unresolved_type_id.index()]; + let mut mapped = None; + for ((pkg_name, item_name), (_iface, ty_stub)) in + unresolved.foreign_unknown.iter() + { + if *ty_stub != unresolved_type_id || item_name != name { + continue; + } + let dep_pkg = resolve.package_names[pkg_name]; + if let Some(type_id) = resolve.packages[dep_pkg].types.get(name).copied() { + mapped = Some(type_id); + } else { + // Resolved as an interface — abandon this type stub. + mapped = None; + } + break; + } + assert_eq!(self.types.len(), unresolved_type_id.index()); + match mapped { + Some(type_id) => self.types.push(Some(type_id)), + None => { + // Either abandoned (resolved as interface) or missing. + // Missing is already reported when processing interfaces. + self.types.push(None); + let _ = span; + } + } + } + TypeOwner::World(_) | TypeOwner::Package(_) => unreachable!(), + } } for (_, ty) in unresolved.types.iter().skip(self.types.len()) { if let TypeDefKind::Unknown = ty.kind { @@ -4468,6 +4593,7 @@ struct MergeMap<'a> { /// * The ID within `from` of the item being added. interfaces_to_add: Vec<(String, PackageId, InterfaceId)>, worlds_to_add: Vec<(String, PackageId, WorldId)>, + types_to_add: Vec<(String, PackageId, TypeId)>, /// Which `Resolve` is being merged from. from: &'a Resolve, @@ -4485,6 +4611,7 @@ impl<'a> MergeMap<'a> { world_map: Default::default(), interfaces_to_add: Default::default(), worlds_to_add: Default::default(), + types_to_add: Default::default(), from, into, } @@ -4527,6 +4654,12 @@ impl<'a> MergeMap<'a> { let into_interface_id = match into.interfaces.get(name) { Some(id) => *id, None => { + if into.types.contains_key(name) || into.worlds.contains_key(name) { + bail!( + "failed to add interface `{name}`: package already has a \ + type or world with that name" + ); + } log::trace!("adding unique interface {name}"); self.interfaces_to_add .push((name.clone(), into_id, *from_interface_id)); @@ -4543,6 +4676,12 @@ impl<'a> MergeMap<'a> { let into_world_id = match into.worlds.get(name) { Some(id) => *id, None => { + if into.types.contains_key(name) || into.interfaces.contains_key(name) { + bail!( + "failed to add world `{name}`: package already has a \ + type or interface with that name" + ); + } log::trace!("adding unique world {name}"); self.worlds_to_add .push((name.clone(), into_id, *from_world_id)); @@ -4555,6 +4694,30 @@ impl<'a> MergeMap<'a> { .with_context(|| format!("failed to merge world `{name}`"))?; } + for (name, from_type_id) in from.types.iter() { + let into_type_id = match into.types.get(name) { + Some(id) => *id, + None => { + if into.interfaces.contains_key(name) || into.worlds.contains_key(name) { + bail!( + "failed to add package type `{name}`: package already has an \ + interface or world with that name" + ); + } + log::trace!("adding unique package type {name}"); + self.types_to_add + .push((name.clone(), into_id, *from_type_id)); + continue; + } + }; + + log::trace!("merging duplicate package types {name}"); + let prev = self.type_map.insert(*from_type_id, into_type_id); + assert!(prev.is_none()); + self.build_type_id(*from_type_id, into_type_id) + .with_context(|| format!("failed to merge package type `{name}`"))?; + } + Ok(()) } diff --git a/crates/wit-parser/tests/package-scope-merge.rs b/crates/wit-parser/tests/package-scope-merge.rs new file mode 100644 index 0000000000..59e3797ce2 --- /dev/null +++ b/crates/wit-parser/tests/package-scope-merge.rs @@ -0,0 +1,227 @@ +use wit_parser::{CloneMaps, Resolve, TypeOwner}; + +#[test] +fn package_scope_types_survive_merge() { + let mut a = Resolve::new(); + let pkg_a = a + .push_str( + "a.wit", + r#" +package local:a; + +record point { + x: u32, + y: u32, +} + +interface api { + move-to: func(p: point); +} + +world w { + export api; +} +"#, + ) + .unwrap(); + + let mut b = Resolve::new(); + let pkg_b = b + .push_str( + "b.wit", + r#" +package local:b; + +enum direction { + north, + south, +} + +interface api { + heading: func() -> direction; +} + +world w { + export api; +} +"#, + ) + .unwrap(); + + let remap = a.merge(b).unwrap(); + let pkg_b = remap.packages[pkg_b.index()]; + + assert_eq!(a.packages[pkg_a].types.len(), 1); + assert!(a.packages[pkg_a].types.contains_key("point")); + assert_eq!(a.packages[pkg_b].types.len(), 1); + assert!(a.packages[pkg_b].types.contains_key("direction")); + + for (name, &id) in a.packages[pkg_a].types.iter() { + assert_eq!(a.types[id].owner, TypeOwner::Package(pkg_a)); + assert_eq!(a.types[id].name.as_deref(), Some(name.as_str())); + } + for (name, &id) in a.packages[pkg_b].types.iter() { + assert_eq!(a.types[id].owner, TypeOwner::Package(pkg_b)); + assert_eq!(a.types[id].name.as_deref(), Some(name.as_str())); + } + + a.assert_valid(); +} + +#[test] +fn package_scope_types_merge_into_same_package() { + let mut into = Resolve::new(); + into.push_str( + "into.wit", + r#" +package local:shared; + +record point { + x: u32, + y: u32, +} + +interface api { + move-to: func(p: point); +} + +world w { + export api; +} +"#, + ) + .unwrap(); + + let mut from = Resolve::new(); + from.push_str( + "from.wit", + r#" +package local:shared; + +record point { + x: u32, + y: u32, +} + +flags style { + bold, + italic, +} + +interface api { + move-to: func(p: point); + style: func() -> style; +} + +world w { + export api; +} +"#, + ) + .unwrap(); + + into.merge(from).unwrap(); + let pkg = *into.package_names.values().next().unwrap(); + assert!(into.packages[pkg].types.contains_key("point")); + assert!(into.packages[pkg].types.contains_key("style")); + into.assert_valid(); +} + +#[test] +fn package_scope_types_survive_merge_worlds() { + let mut resolve = Resolve::new(); + let pkg = resolve + .push_str( + "demo.wit", + r#" +package local:demo; + +record point { + x: u32, + y: u32, +} + +interface a { + move-to: func(p: point); +} + +interface b { + place: func(p: point); +} + +world wa { + export a; +} + +world wb { + export b; +} +"#, + ) + .unwrap(); + + let wa = resolve.packages[pkg].worlds["wa"]; + let wb = resolve.packages[pkg].worlds["wb"]; + let point = resolve.packages[pkg].types["point"]; + + resolve + .merge_worlds(wb, wa, &mut CloneMaps::default()) + .unwrap(); + + assert_eq!(resolve.packages[pkg].types["point"], point); + assert_eq!(resolve.types[point].owner, TypeOwner::Package(pkg)); + assert_eq!(resolve.packages[pkg].types.len(), 1); + resolve.assert_valid(); +} + +#[test] +fn package_scope_type_vs_interface_merge_clash() { + let mut into = Resolve::new(); + into.push_str( + "into.wit", + r#" +package local:shared; + +record point { + x: u32, + y: u32, +} + +interface api { + move-to: func(p: point); +} + +world w { + export api; +} +"#, + ) + .unwrap(); + + let mut from = Resolve::new(); + from.push_str( + "from.wit", + r#" +package local:shared; + +interface point { + get: func() -> u32; +} + +world w { + export point; +} +"#, + ) + .unwrap(); + + let err = match into.merge(from) { + Ok(_) => panic!("expected merge to fail on type vs interface name clash"), + Err(e) => e, + }; + let msg = format!("{err:#}"); + assert!( + msg.contains("point"), + "expected NS clash mentioning `point`, got: {msg}" + ); +} diff --git a/crates/wit-parser/tests/ui/async.wit.json b/crates/wit-parser/tests/ui/async.wit.json index fd009db69b..591be0e9af 100644 --- a/crates/wit-parser/tests/ui/async.wit.json +++ b/crates/wit-parser/tests/ui/async.wit.json @@ -145,7 +145,8 @@ }, "worlds": { "y": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/comments.wit.json b/crates/wit-parser/tests/ui/comments.wit.json index b15b1200f7..f09acee90c 100644 --- a/crates/wit-parser/tests/ui/comments.wit.json +++ b/crates/wit-parser/tests/ui/comments.wit.json @@ -40,7 +40,8 @@ "interfaces": { "foo": 0 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/complex-include.wit.json b/crates/wit-parser/tests/ui/complex-include.wit.json index f9b17f2792..cddf35f7f8 100644 --- a/crates/wit-parser/tests/ui/complex-include.wit.json +++ b/crates/wit-parser/tests/ui/complex-include.wit.json @@ -171,7 +171,8 @@ }, "worlds": { "bar-a": 0 - } + }, + "types": {} }, { "name": "foo:baz", @@ -181,7 +182,8 @@ }, "worlds": { "baz-a": 1 - } + }, + "types": {} }, { "name": "foo:root", @@ -194,7 +196,8 @@ "b": 3, "c": 4, "union-world": 5 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/cross-package-resource.wit.json b/crates/wit-parser/tests/ui/cross-package-resource.wit.json index 51ab92795d..c4b337c882 100644 --- a/crates/wit-parser/tests/ui/cross-package-resource.wit.json +++ b/crates/wit-parser/tests/ui/cross-package-resource.wit.json @@ -54,14 +54,16 @@ "interfaces": { "foo": 0 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:bar", "interfaces": { "foo": 1 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/diamond1.wit.json b/crates/wit-parser/tests/ui/diamond1.wit.json index 54e87e7b04..260a82a7f3 100644 --- a/crates/wit-parser/tests/ui/diamond1.wit.json +++ b/crates/wit-parser/tests/ui/diamond1.wit.json @@ -39,21 +39,24 @@ "interfaces": { "types": 0 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:dep2", "interfaces": { "types": 1 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:foo", "interfaces": {}, "worlds": { "foo": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/disambiguate-diamond.wit.json b/crates/wit-parser/tests/ui/disambiguate-diamond.wit.json index 34f675da51..336a141aae 100644 --- a/crates/wit-parser/tests/ui/disambiguate-diamond.wit.json +++ b/crates/wit-parser/tests/ui/disambiguate-diamond.wit.json @@ -109,7 +109,8 @@ }, "worlds": { "foo": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/empty.wit.json b/crates/wit-parser/tests/ui/empty.wit.json index 092abe5e1d..e46bf51e0d 100644 --- a/crates/wit-parser/tests/ui/empty.wit.json +++ b/crates/wit-parser/tests/ui/empty.wit.json @@ -6,7 +6,8 @@ { "name": "foo:empty", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/error-context.wit.json b/crates/wit-parser/tests/ui/error-context.wit.json index 617e37d162..f2408b1423 100644 --- a/crates/wit-parser/tests/ui/error-context.wit.json +++ b/crates/wit-parser/tests/ui/error-context.wit.json @@ -53,7 +53,8 @@ "interfaces": { "error-contexts": 0 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/feature-gates.wit.json b/crates/wit-parser/tests/ui/feature-gates.wit.json index e959720d9a..4020a830a3 100644 --- a/crates/wit-parser/tests/ui/feature-gates.wit.json +++ b/crates/wit-parser/tests/ui/feature-gates.wit.json @@ -295,7 +295,8 @@ "worlds": { "ungated-world": 0, "mixed-world": 1 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/feature-types.wit.json b/crates/wit-parser/tests/ui/feature-types.wit.json index 929b3982c9..e76b714f51 100644 --- a/crates/wit-parser/tests/ui/feature-types.wit.json +++ b/crates/wit-parser/tests/ui/feature-types.wit.json @@ -60,7 +60,8 @@ "interfaces": { "foo": 0 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/foreign-deps-union.wit.json b/crates/wit-parser/tests/ui/foreign-deps-union.wit.json index a6b2d44acd..8f38e1f930 100644 --- a/crates/wit-parser/tests/ui/foreign-deps-union.wit.json +++ b/crates/wit-parser/tests/ui/foreign-deps-union.wit.json @@ -349,28 +349,32 @@ "interfaces": { "other-interface": 0 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:corp", "interfaces": { "saas": 1 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:different-pkg", "interfaces": { "i": 2 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:foreign-pkg", "interfaces": { "the-default": 3 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:wasi", @@ -380,7 +384,8 @@ }, "worlds": { "wasi": 0 - } + }, + "types": {} }, { "name": "foo:some-pkg", @@ -389,7 +394,8 @@ "some-interface": 7, "another-interface": 8 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:root", @@ -404,7 +410,8 @@ "my-world2": 2, "bars-world": 3, "unionw-world": 4 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/foreign-deps.wit.json b/crates/wit-parser/tests/ui/foreign-deps.wit.json index f24404d86e..f5818bba54 100644 --- a/crates/wit-parser/tests/ui/foreign-deps.wit.json +++ b/crates/wit-parser/tests/ui/foreign-deps.wit.json @@ -304,28 +304,32 @@ "interfaces": { "other-interface": 0 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:corp", "interfaces": { "saas": 1 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:different-pkg", "interfaces": { "i": 2 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:foreign-pkg", "interfaces": { "the-default": 3 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:wasi", @@ -333,7 +337,8 @@ "clocks": 4, "filesystem": 5 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:some-pkg", @@ -342,7 +347,8 @@ "some-interface": 7, "another-interface": 8 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:root", @@ -356,7 +362,8 @@ "my-world": 0, "my-world2": 1, "bars-world": 2 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/foreign-interface-dep-gated.wit.json b/crates/wit-parser/tests/ui/foreign-interface-dep-gated.wit.json index 58e43977ea..9399dec23d 100644 --- a/crates/wit-parser/tests/ui/foreign-interface-dep-gated.wit.json +++ b/crates/wit-parser/tests/ui/foreign-interface-dep-gated.wit.json @@ -19,21 +19,24 @@ { "name": "a:b3", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "a:b2", "interfaces": {}, "worlds": { "the-world": 0 - } + }, + "types": {} }, { "name": "a:b1", "interfaces": {}, "worlds": { "the-world": 1 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/foreign-world-dep-gated.wit.json b/crates/wit-parser/tests/ui/foreign-world-dep-gated.wit.json index 58e43977ea..9399dec23d 100644 --- a/crates/wit-parser/tests/ui/foreign-world-dep-gated.wit.json +++ b/crates/wit-parser/tests/ui/foreign-world-dep-gated.wit.json @@ -19,21 +19,24 @@ { "name": "a:b3", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "a:b2", "interfaces": {}, "worlds": { "the-world": 0 - } + }, + "types": {} }, { "name": "a:b1", "interfaces": {}, "worlds": { "the-world": 1 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/functions.wit.json b/crates/wit-parser/tests/ui/functions.wit.json index b6eb38fdc6..b336608b22 100644 --- a/crates/wit-parser/tests/ui/functions.wit.json +++ b/crates/wit-parser/tests/ui/functions.wit.json @@ -109,7 +109,8 @@ "interfaces": { "functions": 0 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/gated-include.wit.json b/crates/wit-parser/tests/ui/gated-include.wit.json index 5f00420fe1..60d8adb3f0 100644 --- a/crates/wit-parser/tests/ui/gated-include.wit.json +++ b/crates/wit-parser/tests/ui/gated-include.wit.json @@ -340,7 +340,8 @@ "dup-include-in-package": 5, "dup-use-package": 6, "dup-use-package-ordered": 7 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/gated-use.wit.json b/crates/wit-parser/tests/ui/gated-use.wit.json index 7a823b83a4..1b6284ddfb 100644 --- a/crates/wit-parser/tests/ui/gated-use.wit.json +++ b/crates/wit-parser/tests/ui/gated-use.wit.json @@ -21,14 +21,16 @@ "interfaces": { "stable": 0 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "wasmtime:test", "interfaces": { "types": 1 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/ignore-files-deps.wit.json b/crates/wit-parser/tests/ui/ignore-files-deps.wit.json index 7a812dbecf..7178008af3 100644 --- a/crates/wit-parser/tests/ui/ignore-files-deps.wit.json +++ b/crates/wit-parser/tests/ui/ignore-files-deps.wit.json @@ -28,14 +28,16 @@ "interfaces": { "types": 0 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:foo", "interfaces": {}, "worlds": { "foo": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/import-export-overlap1.wit.json b/crates/wit-parser/tests/ui/import-export-overlap1.wit.json index 40fa899882..51f2f8138a 100644 --- a/crates/wit-parser/tests/ui/import-export-overlap1.wit.json +++ b/crates/wit-parser/tests/ui/import-export-overlap1.wit.json @@ -31,7 +31,8 @@ "interfaces": {}, "worlds": { "foo": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/import-export-overlap2.wit.json b/crates/wit-parser/tests/ui/import-export-overlap2.wit.json index 1c0e597444..0fdcc2a3da 100644 --- a/crates/wit-parser/tests/ui/import-export-overlap2.wit.json +++ b/crates/wit-parser/tests/ui/import-export-overlap2.wit.json @@ -36,7 +36,8 @@ "interfaces": {}, "worlds": { "foo": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/include-reps.wit.json b/crates/wit-parser/tests/ui/include-reps.wit.json index 21debef2fe..a6a5760f9a 100644 --- a/crates/wit-parser/tests/ui/include-reps.wit.json +++ b/crates/wit-parser/tests/ui/include-reps.wit.json @@ -62,7 +62,8 @@ "worlds": { "bar": 0, "foo": 1 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/kebab-name-include-with.wit.json b/crates/wit-parser/tests/ui/kebab-name-include-with.wit.json index f940be853b..badd5a6b13 100644 --- a/crates/wit-parser/tests/ui/kebab-name-include-with.wit.json +++ b/crates/wit-parser/tests/ui/kebab-name-include-with.wit.json @@ -60,7 +60,8 @@ "foo": 0, "bar": 1, "baz": 2 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/kinds-of-deps.wit.json b/crates/wit-parser/tests/ui/kinds-of-deps.wit.json index f13afe6301..a71fd72472 100644 --- a/crates/wit-parser/tests/ui/kinds-of-deps.wit.json +++ b/crates/wit-parser/tests/ui/kinds-of-deps.wit.json @@ -61,35 +61,40 @@ "interfaces": { "d": 0 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "e:e", "interfaces": { "e": 1 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "b:b", "interfaces": { "b": 2 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "c:c", "interfaces": { "c": 3 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "a:a", "interfaces": {}, "worlds": { "a": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/many-names.wit.json b/crates/wit-parser/tests/ui/many-names.wit.json index 590c68471a..bea73c3e52 100644 --- a/crates/wit-parser/tests/ui/many-names.wit.json +++ b/crates/wit-parser/tests/ui/many-names.wit.json @@ -36,7 +36,8 @@ }, "worlds": { "name": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/maps.wit.json b/crates/wit-parser/tests/ui/maps.wit.json index 33f5065f12..9a5fa1e79f 100644 --- a/crates/wit-parser/tests/ui/maps.wit.json +++ b/crates/wit-parser/tests/ui/maps.wit.json @@ -249,7 +249,8 @@ }, "worlds": { "maps-world": 0 - } + }, + "types": {} } ] } \ No newline at end of file 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 index 2d0132c82e..6f3529115a 100644 --- a/crates/wit-parser/tests/ui/multi-file-multi-package.wit.json +++ b/crates/wit-parser/tests/ui/multi-file-multi-package.wit.json @@ -217,7 +217,8 @@ }, "worlds": { "w2": 0 - } + }, + "types": {} }, { "name": "baz:name", @@ -226,12 +227,14 @@ }, "worlds": { "w3": 1 - } + }, + "types": {} }, { "name": "foo:main", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:name", @@ -240,7 +243,8 @@ }, "worlds": { "w1": 2 - } + }, + "types": {} }, { "name": "qux:name", @@ -249,7 +253,8 @@ }, "worlds": { "w4": 3 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/multi-file.wit.json b/crates/wit-parser/tests/ui/multi-file.wit.json index 53bb14cb82..d954242876 100644 --- a/crates/wit-parser/tests/ui/multi-file.wit.json +++ b/crates/wit-parser/tests/ui/multi-file.wit.json @@ -294,7 +294,8 @@ "worlds": { "more-depends-on-later-things": 0, "the-world": 1 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/multi-package-deps.wit.json b/crates/wit-parser/tests/ui/multi-package-deps.wit.json index 427c1bdf35..9fa272f36b 100644 --- a/crates/wit-parser/tests/ui/multi-package-deps.wit.json +++ b/crates/wit-parser/tests/ui/multi-package-deps.wit.json @@ -134,28 +134,32 @@ "interfaces": { "i2": 0 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:dep1", "interfaces": { "i1": 1 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:nest", "interfaces": { "nesty": 2 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:root", "interfaces": { "i0": 3 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/multi-package-gated-include.wit.json b/crates/wit-parser/tests/ui/multi-package-gated-include.wit.json index 8beca272e7..3703581aeb 100644 --- a/crates/wit-parser/tests/ui/multi-package-gated-include.wit.json +++ b/crates/wit-parser/tests/ui/multi-package-gated-include.wit.json @@ -638,7 +638,8 @@ }, "worlds": { "imports": 0 - } + }, + "types": {} }, { "name": "wasi:dep-unversioned", @@ -647,7 +648,8 @@ }, "worlds": { "imports": 1 - } + }, + "types": {} }, { "name": "wasi:dep2@0.2.3", @@ -656,14 +658,16 @@ }, "worlds": { "imports": 2 - } + }, + "types": {} }, { "name": "wasi:foo@0.2.3", "interfaces": {}, "worlds": { "imports": 3 - } + }, + "types": {} }, { "name": "wasi:someother@0.2.3", @@ -672,14 +676,16 @@ }, "worlds": { "imports": 4 - } + }, + "types": {} }, { "name": "wasi:unstable@0.2.3", "interfaces": {}, "worlds": { "imports": 5 - } + }, + "types": {} }, { "name": "wasmtime:test", @@ -691,7 +697,8 @@ "test-only-stable": 9, "test-only-stable-with-feature": 10, "test-only-stable-with-in-active-feature": 11 - } + }, + "types": {} } ] } \ No newline at end of file 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 index 8076d19598..540fd190ee 100644 --- a/crates/wit-parser/tests/ui/multi-package-shared-deps.wit.json +++ b/crates/wit-parser/tests/ui/multi-package-shared-deps.wit.json @@ -223,14 +223,16 @@ "interfaces": { "types": 0 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:dep2", "interfaces": { "types": 1 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:bar", @@ -239,7 +241,8 @@ }, "worlds": { "w-bar": 0 - } + }, + "types": {} }, { "name": "foo:qux", @@ -248,14 +251,16 @@ }, "worlds": { "w-qux": 1 - } + }, + "types": {} }, { "name": "foo:root", "interfaces": { "root": 4 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file 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 index ed79fcc057..c06543f034 100644 --- a/crates/wit-parser/tests/ui/multi-package-transitive-deps.wit.json +++ b/crates/wit-parser/tests/ui/multi-package-transitive-deps.wit.json @@ -135,40 +135,46 @@ "interfaces": { "types": 0 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:dep2", "interfaces": { "types": 1 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:dep1", "interfaces": { "types": 2 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:bar", "interfaces": {}, "worlds": { "w-bar": 0 - } + }, + "types": {} }, { "name": "foo:qux", "interfaces": {}, "worlds": { "w-qux": 1 - } + }, + "types": {} }, { "name": "foo:root", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/name-both-resource-and-type.wit.json b/crates/wit-parser/tests/ui/name-both-resource-and-type.wit.json index 1dad5120b5..8ea69f5b81 100644 --- a/crates/wit-parser/tests/ui/name-both-resource-and-type.wit.json +++ b/crates/wit-parser/tests/ui/name-both-resource-and-type.wit.json @@ -76,14 +76,16 @@ "interfaces": { "foo": 0 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:bar", "interfaces": { "foo": 1 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-alias-chain.wit b/crates/wit-parser/tests/ui/package-scope-alias-chain.wit new file mode 100644 index 0000000000..0b3b4a4bad --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-alias-chain.wit @@ -0,0 +1,13 @@ +package local:demo; + +type c = u32; +type b = list; +type a = option; + +interface api { + make: func() -> a; +} + +world w { + export api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-alias-chain.wit.json b/crates/wit-parser/tests/ui/package-scope-alias-chain.wit.json new file mode 100644 index 0000000000..7193743300 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-alias-chain.wit.json @@ -0,0 +1,76 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "api", + "types": {}, + "functions": { + "make": { + "name": "make", + "kind": "freestanding", + "params": [], + "result": 2 + } + }, + "package": 0 + } + ], + "types": [ + { + "name": "c", + "kind": { + "type": "u32" + }, + "owner": { + "package": 0 + } + }, + { + "name": "b", + "kind": { + "list": 0 + }, + "owner": { + "package": 0 + } + }, + { + "name": "a", + "kind": { + "option": 1 + }, + "owner": { + "package": 0 + } + } + ], + "packages": [ + { + "name": "local:demo", + "interfaces": { + "api": 0 + }, + "worlds": { + "w": 0 + }, + "types": { + "c": 0, + "b": 1, + "a": 2 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-all-gated.wit b/crates/wit-parser/tests/ui/package-scope-all-gated.wit new file mode 100644 index 0000000000..64400b308b --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-all-gated.wit @@ -0,0 +1,19 @@ +package local:demo; + +@unstable(feature = inactive) +record hidden { + x: u32, +} + +@unstable(feature = inactive) +flags style { + bold, +} + +interface api { + ping: func(); +} + +world w { + export api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-all-gated.wit.json b/crates/wit-parser/tests/ui/package-scope-all-gated.wit.json new file mode 100644 index 0000000000..2168b039e0 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-all-gated.wit.json @@ -0,0 +1,43 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "api", + "types": {}, + "functions": { + "ping": { + "name": "ping", + "kind": "freestanding", + "params": [] + } + }, + "package": 0 + } + ], + "types": [], + "packages": [ + { + "name": "local:demo", + "interfaces": { + "api": 0 + }, + "worlds": { + "w": 0 + }, + "types": {} + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-deprecated.wit b/crates/wit-parser/tests/ui/package-scope-deprecated.wit new file mode 100644 index 0000000000..9d3bf3b654 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-deprecated.wit @@ -0,0 +1,15 @@ +package local:demo@1.0.0; + +@since(version = 0.1.0) +@deprecated(version = 0.9.0) +record old { + x: u32, +} + +interface api { + f: func(o: old); +} + +world w { + export api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-deprecated.wit.json b/crates/wit-parser/tests/ui/package-scope-deprecated.wit.json new file mode 100644 index 0000000000..a9516e1623 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-deprecated.wit.json @@ -0,0 +1,73 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "api", + "types": {}, + "functions": { + "f": { + "name": "f", + "kind": "freestanding", + "params": [ + { + "name": "o", + "type": 0 + } + ] + } + }, + "package": 0 + } + ], + "types": [ + { + "name": "old", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + }, + "stability": { + "stable": { + "since": "0.1.0", + "deprecated": "0.9.0" + } + } + } + ], + "packages": [ + { + "name": "local:demo@1.0.0", + "interfaces": { + "api": 0 + }, + "worlds": { + "w": 0 + }, + "types": { + "old": 0 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-foreign-as.wit.json b/crates/wit-parser/tests/ui/package-scope-foreign-as.wit.json new file mode 100644 index 0000000000..64c4c612b5 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-foreign-as.wit.json @@ -0,0 +1,85 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-1": { + "interface": { + "id": 1 + } + } + }, + "package": 1 + } + ], + "interfaces": [ + { + "name": "unused", + "types": {}, + "functions": {}, + "package": 0 + }, + { + "name": "api", + "types": {}, + "functions": { + "move-to": { + "name": "move-to", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + }, + "package": 1 + } + ], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + } + ], + "packages": [ + { + "name": "local:types", + "interfaces": { + "unused": 0 + }, + "worlds": {}, + "types": { + "point": 0 + } + }, + { + "name": "local:consumer", + "interfaces": { + "api": 1 + }, + "worlds": { + "w": 0 + }, + "types": {} + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-foreign-as/deps/types.wit b/crates/wit-parser/tests/ui/package-scope-foreign-as/deps/types.wit new file mode 100644 index 0000000000..a2135e771c --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-foreign-as/deps/types.wit @@ -0,0 +1,9 @@ +package local:types; + +record point { + x: u32, + y: u32, +} + +interface unused { +} diff --git a/crates/wit-parser/tests/ui/package-scope-foreign-as/root.wit b/crates/wit-parser/tests/ui/package-scope-foreign-as/root.wit new file mode 100644 index 0000000000..282f0da1da --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-foreign-as/root.wit @@ -0,0 +1,11 @@ +package local:consumer; + +use local:types/point as pt; + +interface api { + move-to: func(p: pt); +} + +world w { + export api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-foreign-compose.wit.json b/crates/wit-parser/tests/ui/package-scope-foreign-compose.wit.json new file mode 100644 index 0000000000..301a2fc7a9 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-foreign-compose.wit.json @@ -0,0 +1,103 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-1": { + "interface": { + "id": 1 + } + } + }, + "package": 1 + } + ], + "interfaces": [ + { + "name": "unused", + "types": {}, + "functions": {}, + "package": 0 + }, + { + "name": "api", + "types": {}, + "functions": { + "wrap": { + "name": "wrap", + "kind": "freestanding", + "params": [ + { + "name": "b", + "type": 1 + } + ] + } + }, + "package": 1 + } + ], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + }, + { + "name": "bin", + "kind": { + "record": { + "fields": [ + { + "name": "p", + "type": 0 + } + ] + } + }, + "owner": { + "package": 1 + } + } + ], + "packages": [ + { + "name": "local:types", + "interfaces": { + "unused": 0 + }, + "worlds": {}, + "types": { + "point": 0 + } + }, + { + "name": "local:consumer", + "interfaces": { + "api": 1 + }, + "worlds": { + "w": 0 + }, + "types": { + "bin": 1 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-foreign-compose/deps/types.wit b/crates/wit-parser/tests/ui/package-scope-foreign-compose/deps/types.wit new file mode 100644 index 0000000000..a2135e771c --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-foreign-compose/deps/types.wit @@ -0,0 +1,9 @@ +package local:types; + +record point { + x: u32, + y: u32, +} + +interface unused { +} diff --git a/crates/wit-parser/tests/ui/package-scope-foreign-compose/root.wit b/crates/wit-parser/tests/ui/package-scope-foreign-compose/root.wit new file mode 100644 index 0000000000..a24b22e823 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-foreign-compose/root.wit @@ -0,0 +1,15 @@ +package local:consumer; + +use local:types/point; + +record bin { + p: point, +} + +interface api { + wrap: func(b: bin); +} + +world w { + export api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-foreign-iface.wit.json b/crates/wit-parser/tests/ui/package-scope-foreign-iface.wit.json new file mode 100644 index 0000000000..30bb4b2002 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-foreign-iface.wit.json @@ -0,0 +1,77 @@ +{ + "worlds": [ + { + "name": "w", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "exports": {}, + "package": 1 + } + ], + "interfaces": [ + { + "name": "api", + "types": {}, + "functions": { + "move-to": { + "name": "move-to", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + }, + "package": 0 + } + ], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + } + ], + "packages": [ + { + "name": "local:types", + "interfaces": { + "api": 0 + }, + "worlds": {}, + "types": { + "point": 0 + } + }, + { + "name": "local:consumer", + "interfaces": {}, + "worlds": { + "w": 0 + }, + "types": {} + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-foreign-iface/deps/types.wit b/crates/wit-parser/tests/ui/package-scope-foreign-iface/deps/types.wit new file mode 100644 index 0000000000..7b274e1ac1 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-foreign-iface/deps/types.wit @@ -0,0 +1,10 @@ +package local:types; + +record point { + x: u32, + y: u32, +} + +interface api { + move-to: func(p: point); +} diff --git a/crates/wit-parser/tests/ui/package-scope-foreign-iface/root.wit b/crates/wit-parser/tests/ui/package-scope-foreign-iface/root.wit new file mode 100644 index 0000000000..71dd65ac70 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-foreign-iface/root.wit @@ -0,0 +1,7 @@ +package local:consumer; + +use local:types/api; + +world w { + import api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-foreign-use.wit.json b/crates/wit-parser/tests/ui/package-scope-foreign-use.wit.json new file mode 100644 index 0000000000..64c4c612b5 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-foreign-use.wit.json @@ -0,0 +1,85 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-1": { + "interface": { + "id": 1 + } + } + }, + "package": 1 + } + ], + "interfaces": [ + { + "name": "unused", + "types": {}, + "functions": {}, + "package": 0 + }, + { + "name": "api", + "types": {}, + "functions": { + "move-to": { + "name": "move-to", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + }, + "package": 1 + } + ], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + } + ], + "packages": [ + { + "name": "local:types", + "interfaces": { + "unused": 0 + }, + "worlds": {}, + "types": { + "point": 0 + } + }, + { + "name": "local:consumer", + "interfaces": { + "api": 1 + }, + "worlds": { + "w": 0 + }, + "types": {} + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-foreign-use/deps/types.wit b/crates/wit-parser/tests/ui/package-scope-foreign-use/deps/types.wit new file mode 100644 index 0000000000..a2135e771c --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-foreign-use/deps/types.wit @@ -0,0 +1,9 @@ +package local:types; + +record point { + x: u32, + y: u32, +} + +interface unused { +} diff --git a/crates/wit-parser/tests/ui/package-scope-foreign-use/root.wit b/crates/wit-parser/tests/ui/package-scope-foreign-use/root.wit new file mode 100644 index 0000000000..2ffea3270d --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-foreign-use/root.wit @@ -0,0 +1,11 @@ +package local:consumer; + +use local:types/point; + +interface api { + move-to: func(p: point); +} + +world w { + export api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-func-name-overlap.wit b/crates/wit-parser/tests/ui/package-scope-func-name-overlap.wit new file mode 100644 index 0000000000..ead31982ad --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-func-name-overlap.wit @@ -0,0 +1,16 @@ +package local:demo; + +flags style { + bold, + italic, +} + +interface api { + /// Function may share a name with a package-scope type; the return type + /// still resolves to the package type. + style: func() -> style; +} + +world w { + export api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-func-name-overlap.wit.json b/crates/wit-parser/tests/ui/package-scope-func-name-overlap.wit.json new file mode 100644 index 0000000000..7c2e9f8213 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-func-name-overlap.wit.json @@ -0,0 +1,68 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "api", + "types": {}, + "functions": { + "style": { + "name": "style", + "kind": "freestanding", + "params": [], + "result": 0, + "docs": { + "contents": "Function may share a name with a package-scope type; the return type\nstill resolves to the package type." + } + } + }, + "package": 0 + } + ], + "types": [ + { + "name": "style", + "kind": { + "flags": { + "flags": [ + { + "name": "bold" + }, + { + "name": "italic" + } + ] + } + }, + "owner": { + "package": 0 + } + } + ], + "packages": [ + { + "name": "local:demo", + "interfaces": { + "api": 0 + }, + "worlds": { + "w": 0 + }, + "types": { + "style": 0 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-gated.wit b/crates/wit-parser/tests/ui/package-scope-gated.wit new file mode 100644 index 0000000000..a9e1fcc8ec --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-gated.wit @@ -0,0 +1,25 @@ +package local:demo; + +@unstable(feature = inactive) +record hidden { + x: u32, +} + +@unstable(feature = active) +record kept { + x: u32, +} + +record point { + x: u32, + y: u32, +} + +interface api { + move-to: func(p: point); + keep: func(k: kept); +} + +world w { + export api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-gated.wit.json b/crates/wit-parser/tests/ui/package-scope-gated.wit.json new file mode 100644 index 0000000000..0c9904e7c8 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-gated.wit.json @@ -0,0 +1,103 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "api", + "types": {}, + "functions": { + "move-to": { + "name": "move-to", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 1 + } + ] + }, + "keep": { + "name": "keep", + "kind": "freestanding", + "params": [ + { + "name": "k", + "type": 0 + } + ] + } + }, + "package": 0 + } + ], + "types": [ + { + "name": "kept", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + }, + "stability": { + "unstable": { + "feature": "active" + } + } + }, + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + } + ], + "packages": [ + { + "name": "local:demo", + "interfaces": { + "api": 0 + }, + "worlds": { + "w": 0 + }, + "types": { + "kept": 0, + "point": 1 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-include-with.wit b/crates/wit-parser/tests/ui/package-scope-include-with.wit new file mode 100644 index 0000000000..822beb4a4e --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-include-with.wit @@ -0,0 +1,15 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +world base { + import move-to: func(p: point); +} + +world w { + include base with { move-to as place } + export trail: func() -> list; +} diff --git a/crates/wit-parser/tests/ui/package-scope-include-with.wit.json b/crates/wit-parser/tests/ui/package-scope-include-with.wit.json new file mode 100644 index 0000000000..0676624236 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-include-with.wit.json @@ -0,0 +1,94 @@ +{ + "worlds": [ + { + "name": "base", + "imports": { + "move-to": { + "function": { + "name": "move-to", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + } + }, + "exports": {}, + "package": 0 + }, + { + "name": "w", + "imports": { + "place": { + "function": { + "name": "place", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + } + }, + "exports": { + "trail": { + "function": { + "name": "trail", + "kind": "freestanding", + "params": [], + "result": 1 + } + } + }, + "package": 0 + } + ], + "interfaces": [], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + }, + { + "name": null, + "kind": { + "list": 0 + }, + "owner": null + } + ], + "packages": [ + { + "name": "local:demo", + "interfaces": {}, + "worlds": { + "base": 0, + "w": 1 + }, + "types": { + "point": 0 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-include.wit b/crates/wit-parser/tests/ui/package-scope-include.wit new file mode 100644 index 0000000000..b7e001d928 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-include.wit @@ -0,0 +1,15 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +world base { + import move-to: func(p: point); +} + +world w { + include base; + export place: func(p: point); +} diff --git a/crates/wit-parser/tests/ui/package-scope-include.wit.json b/crates/wit-parser/tests/ui/package-scope-include.wit.json new file mode 100644 index 0000000000..5b5087f3d6 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-include.wit.json @@ -0,0 +1,91 @@ +{ + "worlds": [ + { + "name": "base", + "imports": { + "move-to": { + "function": { + "name": "move-to", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + } + }, + "exports": {}, + "package": 0 + }, + { + "name": "w", + "imports": { + "move-to": { + "function": { + "name": "move-to", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + } + }, + "exports": { + "place": { + "function": { + "name": "place", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + } + }, + "package": 0 + } + ], + "interfaces": [], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + } + ], + "packages": [ + { + "name": "local:demo", + "interfaces": {}, + "worlds": { + "base": 0, + "w": 1 + }, + "types": { + "point": 0 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-local-use.wit b/crates/wit-parser/tests/ui/package-scope-local-use.wit new file mode 100644 index 0000000000..9cd602b4e5 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-local-use.wit @@ -0,0 +1,16 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +use point as pt; + +interface api { + move-to: func(p: pt); +} + +world w { + export api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-local-use.wit.json b/crates/wit-parser/tests/ui/package-scope-local-use.wit.json new file mode 100644 index 0000000000..3cf57db27c --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-local-use.wit.json @@ -0,0 +1,71 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "api", + "types": {}, + "functions": { + "move-to": { + "name": "move-to", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + }, + "package": 0 + } + ], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + } + ], + "packages": [ + { + "name": "local:demo", + "interfaces": { + "api": 0 + }, + "worlds": { + "w": 0 + }, + "types": { + "point": 0 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-multifile-world.wit.json b/crates/wit-parser/tests/ui/package-scope-multifile-world.wit.json new file mode 100644 index 0000000000..712545b77a --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-multifile-world.wit.json @@ -0,0 +1,74 @@ +{ + "worlds": [ + { + "name": "w", + "imports": { + "place": { + "function": { + "name": "place", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + } + }, + "exports": { + "trail": { + "function": { + "name": "trail", + "kind": "freestanding", + "params": [], + "result": 1 + } + } + }, + "package": 0 + } + ], + "interfaces": [], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + }, + { + "name": null, + "kind": { + "list": 0 + }, + "owner": null + } + ], + "packages": [ + { + "name": "local:demo", + "interfaces": {}, + "worlds": { + "w": 0 + }, + "types": { + "point": 0 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-multifile-world/types.wit b/crates/wit-parser/tests/ui/package-scope-multifile-world/types.wit new file mode 100644 index 0000000000..d58d3b8f68 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-multifile-world/types.wit @@ -0,0 +1,6 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} diff --git a/crates/wit-parser/tests/ui/package-scope-multifile-world/world.wit b/crates/wit-parser/tests/ui/package-scope-multifile-world/world.wit new file mode 100644 index 0000000000..bc9469b688 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-multifile-world/world.wit @@ -0,0 +1,6 @@ +package local:demo; + +world w { + import place: func(p: point); + export trail: func() -> list; +} diff --git a/crates/wit-parser/tests/ui/package-scope-multifile.wit.json b/crates/wit-parser/tests/ui/package-scope-multifile.wit.json new file mode 100644 index 0000000000..3cf57db27c --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-multifile.wit.json @@ -0,0 +1,71 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "api", + "types": {}, + "functions": { + "move-to": { + "name": "move-to", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + }, + "package": 0 + } + ], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + } + ], + "packages": [ + { + "name": "local:demo", + "interfaces": { + "api": 0 + }, + "worlds": { + "w": 0 + }, + "types": { + "point": 0 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-multifile/api.wit b/crates/wit-parser/tests/ui/package-scope-multifile/api.wit new file mode 100644 index 0000000000..6c1d05d6c0 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-multifile/api.wit @@ -0,0 +1,9 @@ +package local:demo; + +interface api { + move-to: func(p: point); +} + +world w { + export api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-multifile/types.wit b/crates/wit-parser/tests/ui/package-scope-multifile/types.wit new file mode 100644 index 0000000000..d58d3b8f68 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-multifile/types.wit @@ -0,0 +1,6 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} diff --git a/crates/wit-parser/tests/ui/package-scope-nested-use.wit b/crates/wit-parser/tests/ui/package-scope-nested-use.wit new file mode 100644 index 0000000000..f185fd07f2 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-nested-use.wit @@ -0,0 +1,23 @@ +package local:root; + +package local:nested { + record point { + x: u32, + y: u32, + } + + interface unused { + } +} + +package local:consumer { + use local:nested/point; + + interface api { + move-to: func(p: point); + } + + world w { + export api; + } +} diff --git a/crates/wit-parser/tests/ui/package-scope-nested-use.wit.json b/crates/wit-parser/tests/ui/package-scope-nested-use.wit.json new file mode 100644 index 0000000000..f0fb8d8d55 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-nested-use.wit.json @@ -0,0 +1,91 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-1": { + "interface": { + "id": 1 + } + } + }, + "package": 1 + } + ], + "interfaces": [ + { + "name": "unused", + "types": {}, + "functions": {}, + "package": 0 + }, + { + "name": "api", + "types": {}, + "functions": { + "move-to": { + "name": "move-to", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + }, + "package": 1 + } + ], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + } + ], + "packages": [ + { + "name": "local:nested", + "interfaces": { + "unused": 0 + }, + "worlds": {}, + "types": { + "point": 0 + } + }, + { + "name": "local:consumer", + "interfaces": { + "api": 1 + }, + "worlds": { + "w": 0 + }, + "types": {} + }, + { + "name": "local:root", + "interfaces": {}, + "worlds": {}, + "types": {} + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-nested.wit b/crates/wit-parser/tests/ui/package-scope-nested.wit new file mode 100644 index 0000000000..db0b7d7fec --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-nested.wit @@ -0,0 +1,16 @@ +package local:root; + +package local:nested { + record point { + x: u32, + y: u32, + } + + interface api { + move-to: func(p: point); + } + + world w { + export api; + } +} diff --git a/crates/wit-parser/tests/ui/package-scope-nested.wit.json b/crates/wit-parser/tests/ui/package-scope-nested.wit.json new file mode 100644 index 0000000000..ab08797961 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-nested.wit.json @@ -0,0 +1,77 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "api", + "types": {}, + "functions": { + "move-to": { + "name": "move-to", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + }, + "package": 0 + } + ], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + } + ], + "packages": [ + { + "name": "local:nested", + "interfaces": { + "api": 0 + }, + "worlds": { + "w": 0 + }, + "types": { + "point": 0 + } + }, + { + "name": "local:root", + "interfaces": {}, + "worlds": {}, + "types": {} + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-rich-aliases.wit b/crates/wit-parser/tests/ui/package-scope-rich-aliases.wit new file mode 100644 index 0000000000..6b10550006 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-rich-aliases.wit @@ -0,0 +1,16 @@ +package local:demo; + +type pair = tuple; +type maybe = option; +type either = result; +type pipe = stream; +type job = future; +type table = map; + +interface api { + f: func() -> tuple; +} + +world w { + export api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-rich-aliases.wit.json b/crates/wit-parser/tests/ui/package-scope-rich-aliases.wit.json new file mode 100644 index 0000000000..2bff097688 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-rich-aliases.wit.json @@ -0,0 +1,132 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "api", + "types": {}, + "functions": { + "f": { + "name": "f", + "kind": "freestanding", + "params": [], + "result": 6 + } + }, + "package": 0 + } + ], + "types": [ + { + "name": "pair", + "kind": { + "tuple": { + "types": [ + "u32", + "u32" + ] + } + }, + "owner": { + "package": 0 + } + }, + { + "name": "maybe", + "kind": { + "option": 0 + }, + "owner": { + "package": 0 + } + }, + { + "name": "either", + "kind": { + "result": { + "ok": 0, + "err": "string" + } + }, + "owner": { + "package": 0 + } + }, + { + "name": "pipe", + "kind": { + "stream": "u8" + }, + "owner": { + "package": 0 + } + }, + { + "name": "job", + "kind": { + "future": 0 + }, + "owner": { + "package": 0 + } + }, + { + "name": "table", + "kind": { + "map": [ + "string", + 0 + ] + }, + "owner": { + "package": 0 + } + }, + { + "name": null, + "kind": { + "tuple": { + "types": [ + 1, + 2, + 3, + 4, + 5 + ] + } + }, + "owner": null + } + ], + "packages": [ + { + "name": "local:demo", + "interfaces": { + "api": 0 + }, + "worlds": { + "w": 0 + }, + "types": { + "pair": 0, + "maybe": 1, + "either": 2, + "pipe": 3, + "job": 4, + "table": 5 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-self-use.wit b/crates/wit-parser/tests/ui/package-scope-self-use.wit new file mode 100644 index 0000000000..c470b3d372 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-self-use.wit @@ -0,0 +1,16 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +use local:demo/point as pt; + +interface api { + move-to: func(p: pt); +} + +world w { + export api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-self-use.wit.json b/crates/wit-parser/tests/ui/package-scope-self-use.wit.json new file mode 100644 index 0000000000..3cf57db27c --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-self-use.wit.json @@ -0,0 +1,71 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "api", + "types": {}, + "functions": { + "move-to": { + "name": "move-to", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + }, + "package": 0 + } + ], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + } + ], + "packages": [ + { + "name": "local:demo", + "interfaces": { + "api": 0 + }, + "worlds": { + "w": 0 + }, + "types": { + "point": 0 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-shadow.wit b/crates/wit-parser/tests/ui/package-scope-shadow.wit new file mode 100644 index 0000000000..1372ea5b12 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-shadow.wit @@ -0,0 +1,15 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +interface api { + type point = u64; + f: func(p: point); +} + +world w { + export api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-shadow.wit.json b/crates/wit-parser/tests/ui/package-scope-shadow.wit.json new file mode 100644 index 0000000000..6573195c00 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-shadow.wit.json @@ -0,0 +1,82 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "api", + "types": { + "point": 1 + }, + "functions": { + "f": { + "name": "f", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 1 + } + ] + } + }, + "package": 0 + } + ], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + }, + { + "name": "point", + "kind": { + "type": "u64" + }, + "owner": { + "interface": 0 + } + } + ], + "packages": [ + { + "name": "local:demo", + "interfaces": { + "api": 0 + }, + "worlds": { + "w": 0 + }, + "types": { + "point": 0 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-since.wit b/crates/wit-parser/tests/ui/package-scope-since.wit new file mode 100644 index 0000000000..d2ac75b251 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-since.wit @@ -0,0 +1,20 @@ +package local:demo@1.0.0; + +@since(version = 0.2.0) +record kept { + x: u32, +} + +@since(version = 1.0.0) +record also-kept { + x: u32, +} + +interface api { + f: func(k: kept); + g: func(a: also-kept); +} + +world w { + export api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-since.wit.json b/crates/wit-parser/tests/ui/package-scope-since.wit.json new file mode 100644 index 0000000000..54c46f34bf --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-since.wit.json @@ -0,0 +1,104 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "api", + "types": {}, + "functions": { + "f": { + "name": "f", + "kind": "freestanding", + "params": [ + { + "name": "k", + "type": 0 + } + ] + }, + "g": { + "name": "g", + "kind": "freestanding", + "params": [ + { + "name": "a", + "type": 1 + } + ] + } + }, + "package": 0 + } + ], + "types": [ + { + "name": "kept", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + }, + "stability": { + "stable": { + "since": "0.2.0" + } + } + }, + { + "name": "also-kept", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + }, + "stability": { + "stable": { + "since": "1.0.0" + } + } + } + ], + "packages": [ + { + "name": "local:demo@1.0.0", + "interfaces": { + "api": 0 + }, + "worlds": { + "w": 0 + }, + "types": { + "kept": 0, + "also-kept": 1 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-types.wit b/crates/wit-parser/tests/ui/package-scope-types.wit new file mode 100644 index 0000000000..9cfceb3d33 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-types.wit @@ -0,0 +1,39 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +variant shape { + circle(u32), + rect(point), +} + +flags style { + bold, + italic, +} + +enum direction { + north, + south, + east, + west, +} + +type path = list; + +interface api { + move-to: func(p: point); + heading: func() -> direction; + trail: func() -> path; + draw: func(s: shape, style: style); +} + +world w { + import host: interface { + place: func(p: point); + } + export api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-types.wit.json b/crates/wit-parser/tests/ui/package-scope-types.wit.json new file mode 100644 index 0000000000..a49f6f526c --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-types.wit.json @@ -0,0 +1,195 @@ +{ + "worlds": [ + { + "name": "w", + "imports": { + "host": { + "interface": { + "id": 1 + } + } + }, + "exports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "api", + "types": {}, + "functions": { + "move-to": { + "name": "move-to", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + }, + "heading": { + "name": "heading", + "kind": "freestanding", + "params": [], + "result": 3 + }, + "trail": { + "name": "trail", + "kind": "freestanding", + "params": [], + "result": 4 + }, + "draw": { + "name": "draw", + "kind": "freestanding", + "params": [ + { + "name": "s", + "type": 1 + }, + { + "name": "style", + "type": 2 + } + ] + } + }, + "package": 0 + }, + { + "name": null, + "types": {}, + "functions": { + "place": { + "name": "place", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + }, + "package": 0 + } + ], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + }, + { + "name": "shape", + "kind": { + "variant": { + "cases": [ + { + "name": "circle", + "type": "u32" + }, + { + "name": "rect", + "type": 0 + } + ] + } + }, + "owner": { + "package": 0 + } + }, + { + "name": "style", + "kind": { + "flags": { + "flags": [ + { + "name": "bold" + }, + { + "name": "italic" + } + ] + } + }, + "owner": { + "package": 0 + } + }, + { + "name": "direction", + "kind": { + "enum": { + "cases": [ + { + "name": "north" + }, + { + "name": "south" + }, + { + "name": "east" + }, + { + "name": "west" + } + ] + } + }, + "owner": { + "package": 0 + } + }, + { + "name": "path", + "kind": { + "list": 0 + }, + "owner": { + "package": 0 + } + } + ], + "packages": [ + { + "name": "local:demo", + "interfaces": { + "api": 0 + }, + "worlds": { + "w": 0 + }, + "types": { + "point": 0, + "shape": 1, + "style": 2, + "direction": 3, + "path": 4 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-versioned-use.wit.json b/crates/wit-parser/tests/ui/package-scope-versioned-use.wit.json new file mode 100644 index 0000000000..e52f52e2b4 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-versioned-use.wit.json @@ -0,0 +1,85 @@ +{ + "worlds": [ + { + "name": "w", + "imports": {}, + "exports": { + "interface-1": { + "interface": { + "id": 1 + } + } + }, + "package": 1 + } + ], + "interfaces": [ + { + "name": "unused", + "types": {}, + "functions": {}, + "package": 0 + }, + { + "name": "api", + "types": {}, + "functions": { + "move-to": { + "name": "move-to", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + }, + "package": 1 + } + ], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + } + ], + "packages": [ + { + "name": "local:types@1.0.0", + "interfaces": { + "unused": 0 + }, + "worlds": {}, + "types": { + "point": 0 + } + }, + { + "name": "local:consumer", + "interfaces": { + "api": 1 + }, + "worlds": { + "w": 0 + }, + "types": {} + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-scope-versioned-use/deps/types.wit b/crates/wit-parser/tests/ui/package-scope-versioned-use/deps/types.wit new file mode 100644 index 0000000000..ee15458642 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-versioned-use/deps/types.wit @@ -0,0 +1,9 @@ +package local:types@1.0.0; + +record point { + x: u32, + y: u32, +} + +interface unused { +} diff --git a/crates/wit-parser/tests/ui/package-scope-versioned-use/root.wit b/crates/wit-parser/tests/ui/package-scope-versioned-use/root.wit new file mode 100644 index 0000000000..6963614cb4 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-versioned-use/root.wit @@ -0,0 +1,11 @@ +package local:consumer; + +use local:types/point@1.0.0; + +interface api { + move-to: func(p: point); +} + +world w { + export api; +} diff --git a/crates/wit-parser/tests/ui/package-scope-world.wit b/crates/wit-parser/tests/ui/package-scope-world.wit new file mode 100644 index 0000000000..3a7b29968d --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-world.wit @@ -0,0 +1,12 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +world w { + import move-to: func(p: point); + export place: func(p: point); + export trail: func() -> list; +} diff --git a/crates/wit-parser/tests/ui/package-scope-world.wit.json b/crates/wit-parser/tests/ui/package-scope-world.wit.json new file mode 100644 index 0000000000..252cda7997 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-world.wit.json @@ -0,0 +1,86 @@ +{ + "worlds": [ + { + "name": "w", + "imports": { + "move-to": { + "function": { + "name": "move-to", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + } + }, + "exports": { + "place": { + "function": { + "name": "place", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + }, + "trail": { + "function": { + "name": "trail", + "kind": "freestanding", + "params": [], + "result": 1 + } + } + }, + "package": 0 + } + ], + "interfaces": [], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + }, + { + "name": null, + "kind": { + "list": 0 + }, + "owner": null + } + ], + "packages": [ + { + "name": "local:demo", + "interfaces": {}, + "worlds": { + "w": 0 + }, + "types": { + "point": 0 + } + } + ] +} \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-syntax1.wit.json b/crates/wit-parser/tests/ui/package-syntax1.wit.json index 80af0cbb3b..028ca2a78c 100644 --- a/crates/wit-parser/tests/ui/package-syntax1.wit.json +++ b/crates/wit-parser/tests/ui/package-syntax1.wit.json @@ -6,7 +6,8 @@ { "name": "foo:foo", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-syntax3.wit.json b/crates/wit-parser/tests/ui/package-syntax3.wit.json index e0323c511b..3684a49107 100644 --- a/crates/wit-parser/tests/ui/package-syntax3.wit.json +++ b/crates/wit-parser/tests/ui/package-syntax3.wit.json @@ -6,7 +6,8 @@ { "name": "foo:bar", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/package-syntax4.wit.json b/crates/wit-parser/tests/ui/package-syntax4.wit.json index 7bacc86f03..109587e3b1 100644 --- a/crates/wit-parser/tests/ui/package-syntax4.wit.json +++ b/crates/wit-parser/tests/ui/package-syntax4.wit.json @@ -6,7 +6,8 @@ { "name": "foo:bar@2.0.0", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/packages-multiple-nested.wit.json b/crates/wit-parser/tests/ui/packages-multiple-nested.wit.json index f2c8d6b0ee..ac40eb7a5f 100644 --- a/crates/wit-parser/tests/ui/packages-multiple-nested.wit.json +++ b/crates/wit-parser/tests/ui/packages-multiple-nested.wit.json @@ -160,7 +160,8 @@ }, "worlds": { "w2": 0 - } + }, + "types": {} }, { "name": "foo:name", @@ -169,7 +170,8 @@ }, "worlds": { "w1": 1 - } + }, + "types": {} }, { "name": "foo:root", @@ -178,7 +180,8 @@ }, "worlds": { "w0": 2 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/packages-nested-colliding-decl-names.wit.json b/crates/wit-parser/tests/ui/packages-nested-colliding-decl-names.wit.json index 3a9dc3b079..4b0f98c0aa 100644 --- a/crates/wit-parser/tests/ui/packages-nested-colliding-decl-names.wit.json +++ b/crates/wit-parser/tests/ui/packages-nested-colliding-decl-names.wit.json @@ -115,7 +115,8 @@ }, "worlds": { "w": 0 - } + }, + "types": {} }, { "name": "foo:name", @@ -124,12 +125,14 @@ }, "worlds": { "w": 1 - } + }, + "types": {} }, { "name": "foo:root", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/packages-nested-internal-references.wit.json b/crates/wit-parser/tests/ui/packages-nested-internal-references.wit.json index d11a121536..b7ddf22e4d 100644 --- a/crates/wit-parser/tests/ui/packages-nested-internal-references.wit.json +++ b/crates/wit-parser/tests/ui/packages-nested-internal-references.wit.json @@ -73,19 +73,22 @@ "interfaces": { "i1": 0 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "bar:name", "interfaces": {}, "worlds": { "w1": 0 - } + }, + "types": {} }, { "name": "foo:root", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/packages-nested-with-semver.wit.json b/crates/wit-parser/tests/ui/packages-nested-with-semver.wit.json index ecd0fe37ce..318135d497 100644 --- a/crates/wit-parser/tests/ui/packages-nested-with-semver.wit.json +++ b/crates/wit-parser/tests/ui/packages-nested-with-semver.wit.json @@ -115,7 +115,8 @@ }, "worlds": { "w1": 0 - } + }, + "types": {} }, { "name": "foo:name@1.0.1", @@ -124,12 +125,14 @@ }, "worlds": { "w1": 1 - } + }, + "types": {} }, { "name": "foo:root", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/packages-single-nested.wit.json b/crates/wit-parser/tests/ui/packages-single-nested.wit.json index c67c2a78f9..218917023f 100644 --- a/crates/wit-parser/tests/ui/packages-single-nested.wit.json +++ b/crates/wit-parser/tests/ui/packages-single-nested.wit.json @@ -64,12 +64,14 @@ }, "worlds": { "w1": 0 - } + }, + "types": {} }, { "name": "foo:root", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ 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 91c398043d..7c8f4c40c9 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,4 +1,4 @@ -failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/no-access-to-sibling-use]: failed to parse package: tests/ui/parse-fail/no-access-to-sibling-use: interface or world `bar-renamed` does not exist +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/no-access-to-sibling-use]: failed to parse package: tests/ui/parse-fail/no-access-to-sibling-use: interface, world, or type `bar-renamed` does not exist --> tests/ui/parse-fail/no-access-to-sibling-use/foo.wit:3:5 | 3 | use bar-renamed; diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-dup-multifile.wit.result b/crates/wit-parser/tests/ui/parse-fail/package-scope-dup-multifile.wit.result new file mode 100644 index 0000000000..e7bc9f03ff --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-dup-multifile.wit.result @@ -0,0 +1,5 @@ +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/package-scope-dup-multifile]: failed to parse package: tests/ui/parse-fail/package-scope-dup-multifile: duplicate item named `point` + --> tests/ui/parse-fail/package-scope-dup-multifile/types2.wit:3:8 + | + 3 | record point { + | ^---- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-dup-multifile/types1.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-dup-multifile/types1.wit new file mode 100644 index 0000000000..d58d3b8f68 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-dup-multifile/types1.wit @@ -0,0 +1,6 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-dup-multifile/types2.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-dup-multifile/types2.wit new file mode 100644 index 0000000000..d58d3b8f68 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-dup-multifile/types2.wit @@ -0,0 +1,6 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-gated-ref.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-gated-ref.wit new file mode 100644 index 0000000000..f07648fbfe --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-gated-ref.wit @@ -0,0 +1,10 @@ +package local:demo; + +@unstable(feature = inactive) +record hidden { + x: u32, +} + +interface api { + use-hidden: func(h: hidden); +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-gated-ref.wit.result b/crates/wit-parser/tests/ui/parse-fail/package-scope-gated-ref.wit.result new file mode 100644 index 0000000000..4d5334453e --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-gated-ref.wit.result @@ -0,0 +1,5 @@ +found a reference to a type which is excluded due to its feature not being activated + --> tests/ui/parse-fail/package-scope-gated-ref.wit:9:3 + | + 9 | use-hidden: func(h: hidden); + | ^--------- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-multifile-ns-clash.wit.result b/crates/wit-parser/tests/ui/parse-fail/package-scope-multifile-ns-clash.wit.result new file mode 100644 index 0000000000..d83a86933f --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-multifile-ns-clash.wit.result @@ -0,0 +1,5 @@ +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/package-scope-multifile-ns-clash]: failed to parse package: tests/ui/parse-fail/package-scope-multifile-ns-clash: duplicate item named `point` + --> tests/ui/parse-fail/package-scope-multifile-ns-clash/types.wit:3:8 + | + 3 | record point { + | ^---- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-multifile-ns-clash/api.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-multifile-ns-clash/api.wit new file mode 100644 index 0000000000..315362aca0 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-multifile-ns-clash/api.wit @@ -0,0 +1,5 @@ +package local:demo; + +interface point { + get: func() -> u32; +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-multifile-ns-clash/types.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-multifile-ns-clash/types.wit new file mode 100644 index 0000000000..d58d3b8f68 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-multifile-ns-clash/types.wit @@ -0,0 +1,6 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-nested-outer-leak.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-nested-outer-leak.wit new file mode 100644 index 0000000000..00737d5490 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-nested-outer-leak.wit @@ -0,0 +1,17 @@ +package local:root; + +package local:nested { + record point { + x: u32, + y: u32, + } + + interface api { + move-to: func(p: point); + } +} + +interface outer { + // Nested package types must not leak into the outer package without a use. + bad: func(p: point); +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-nested-outer-leak.wit.result b/crates/wit-parser/tests/ui/parse-fail/package-scope-nested-outer-leak.wit.result new file mode 100644 index 0000000000..a8d21e191e --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-nested-outer-leak.wit.result @@ -0,0 +1,5 @@ +name `point` does not exist + --> tests/ui/parse-fail/package-scope-nested-outer-leak.wit:16:16 + | + 16 | bad: func(p: point); + | ^---- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-resource.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-resource.wit new file mode 100644 index 0000000000..92c635ffec --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-resource.wit @@ -0,0 +1,4 @@ +package local:demo; + +resource r { +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-resource.wit.result b/crates/wit-parser/tests/ui/parse-fail/package-scope-resource.wit.result new file mode 100644 index 0000000000..a22f3cd321 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-resource.wit.result @@ -0,0 +1,5 @@ +resources cannot be declared at package scope + --> tests/ui/parse-fail/package-scope-resource.wit:3:1 + | + 3 | resource r { + | ^------- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-self-use-dup.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-self-use-dup.wit new file mode 100644 index 0000000000..b54444daad --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-self-use-dup.wit @@ -0,0 +1,14 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +// Package type `point` already occupies this file's namespace, so a plain +// self-use without `as` is a duplicate name (use `as` to alias instead). +use local:demo/point; + +interface api { + move-to: func(p: point); +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-self-use-dup.wit.result b/crates/wit-parser/tests/ui/parse-fail/package-scope-self-use-dup.wit.result new file mode 100644 index 0000000000..aab910b6c1 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-self-use-dup.wit.result @@ -0,0 +1,5 @@ +duplicate name `point` in this file + --> tests/ui/parse-fail/package-scope-self-use-dup.wit:10:16 + | + 10 | use local:demo/point; + | ^---- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-since-ref.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-since-ref.wit new file mode 100644 index 0000000000..f63f408b8a --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-since-ref.wit @@ -0,0 +1,10 @@ +package local:demo@1.0.0; + +@since(version = 1.1.0) +record upcoming { + x: u32, +} + +interface api { + f: func(k: upcoming); +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-since-ref.wit.result b/crates/wit-parser/tests/ui/parse-fail/package-scope-since-ref.wit.result new file mode 100644 index 0000000000..4e3667bbc9 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-since-ref.wit.result @@ -0,0 +1,5 @@ +feature gate cannot reference unreleased version 1.1.0 of package [local:demo@1.0.0] (current version 1.0.0) + --> tests/ui/parse-fail/package-scope-since-ref.wit:4:8 + | + 4 | record upcoming { + | ^------- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-type-cycle.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-type-cycle.wit new file mode 100644 index 0000000000..9098e942e2 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-type-cycle.wit @@ -0,0 +1,4 @@ +package local:demo; + +type a = list; +type b = list; diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-type-cycle.wit.result b/crates/wit-parser/tests/ui/parse-fail/package-scope-type-cycle.wit.result new file mode 100644 index 0000000000..ee26c1c80c --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-type-cycle.wit.result @@ -0,0 +1,5 @@ +type `b` depends on itself + --> tests/ui/parse-fail/package-scope-type-cycle.wit:3:15 + | + 3 | type a = list; + | ^ \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-type-iface-clash.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-type-iface-clash.wit new file mode 100644 index 0000000000..48f65322bc --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-type-iface-clash.wit @@ -0,0 +1,10 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +interface point { + get: func() -> u32; +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-type-iface-clash.wit.result b/crates/wit-parser/tests/ui/parse-fail/package-scope-type-iface-clash.wit.result new file mode 100644 index 0000000000..1a633dbc97 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-type-iface-clash.wit.result @@ -0,0 +1,5 @@ +duplicate item named `point` + --> tests/ui/parse-fail/package-scope-type-iface-clash.wit:8:11 + | + 8 | interface point { + | ^---- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-type-world-clash.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-type-world-clash.wit new file mode 100644 index 0000000000..1357646751 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-type-world-clash.wit @@ -0,0 +1,10 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +world point { + import foo: interface {} +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-type-world-clash.wit.result b/crates/wit-parser/tests/ui/parse-fail/package-scope-type-world-clash.wit.result new file mode 100644 index 0000000000..add3c52612 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-type-world-clash.wit.result @@ -0,0 +1,5 @@ +duplicate item named `point` + --> tests/ui/parse-fail/package-scope-type-world-clash.wit:8:7 + | + 8 | world point { + | ^---- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-use-cross-file.wit.result b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-cross-file.wit.result new file mode 100644 index 0000000000..55bd324086 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-cross-file.wit.result @@ -0,0 +1,5 @@ +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/package-scope-use-cross-file]: failed to parse package: tests/ui/parse-fail/package-scope-use-cross-file: name `point` does not exist + --> tests/ui/parse-fail/package-scope-use-cross-file/types.wit:6:6 + | + 6 | p: point, + | ^---- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-use-cross-file/deps/foreign.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-cross-file/deps/foreign.wit new file mode 100644 index 0000000000..c565aec476 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-cross-file/deps/foreign.wit @@ -0,0 +1,9 @@ +package local:foreign; + +record point { + x: u32, + y: u32, +} + +interface unused { +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-use-cross-file/types.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-cross-file/types.wit new file mode 100644 index 0000000000..b30dc8c78d --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-cross-file/types.wit @@ -0,0 +1,11 @@ +package local:demo; + +// `point` was imported in uses.wit; toplevel use is file-scoped, so this +// package-scope type must not see that alias. +record bin { + p: point, +} + +interface api { + wrap: func(b: bin); +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-use-cross-file/uses.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-cross-file/uses.wit new file mode 100644 index 0000000000..e87e567929 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-cross-file/uses.wit @@ -0,0 +1,3 @@ +package local:demo; + +use local:foreign/point; diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-use-missing.wit.result b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-missing.wit.result new file mode 100644 index 0000000000..382c69a1b3 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-missing.wit.result @@ -0,0 +1,5 @@ +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/package-scope-use-missing]: interface or type `missing` not found in package `local:types` + --> tests/ui/parse-fail/package-scope-use-missing/root.wit:3:17 + | + 3 | use local:types/missing; + | ^------ \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-use-missing/deps/types.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-missing/deps/types.wit new file mode 100644 index 0000000000..de228f0084 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-missing/deps/types.wit @@ -0,0 +1,5 @@ +package local:types; + +interface api { + f: func(); +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-use-missing/root.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-missing/root.wit new file mode 100644 index 0000000000..865cd5e556 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-missing/root.wit @@ -0,0 +1,7 @@ +package local:consumer; + +use local:types/missing; + +interface api { + f: func(); +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface-foreign.wit.result b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface-foreign.wit.result new file mode 100644 index 0000000000..de8b3f6e91 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface-foreign.wit.result @@ -0,0 +1,5 @@ +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/package-scope-use-type-as-iface-foreign]: name `point` is defined as a type, not an interface + --> tests/ui/parse-fail/package-scope-use-type-as-iface-foreign/root.wit:4:19 + | + 4 | use local:types/point.{x}; + | ^---- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface-foreign/deps/types.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface-foreign/deps/types.wit new file mode 100644 index 0000000000..a2135e771c --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface-foreign/deps/types.wit @@ -0,0 +1,9 @@ +package local:types; + +record point { + x: u32, + y: u32, +} + +interface unused { +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface-foreign/root.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface-foreign/root.wit new file mode 100644 index 0000000000..ead92cd5a8 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface-foreign/root.wit @@ -0,0 +1,6 @@ +package local:consumer; + +interface api { + use local:types/point.{x}; + f: func(); +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface.wit new file mode 100644 index 0000000000..bf9c43a79a --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface.wit @@ -0,0 +1,10 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +interface api { + use point.{x}; +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface.wit.result b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface.wit.result new file mode 100644 index 0000000000..4174a4b117 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-type-as-iface.wit.result @@ -0,0 +1,5 @@ +name `point` is defined as a type, not an interface + --> tests/ui/parse-fail/package-scope-use-type-as-iface.wit:9:7 + | + 9 | use point.{x}; + | ^---- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-use-world.wit.result b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-world.wit.result new file mode 100644 index 0000000000..2125434f1d --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-world.wit.result @@ -0,0 +1,5 @@ +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/package-scope-use-world]: `point` is a world; top-level use expects an interface or type + --> tests/ui/parse-fail/package-scope-use-world/root.wit:3:17 + | + 3 | use local:types/point; + | ^---- \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-use-world/deps/types.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-world/deps/types.wit new file mode 100644 index 0000000000..471becfa20 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-world/deps/types.wit @@ -0,0 +1,5 @@ +package local:types; + +world point { + import foo: interface {} +} diff --git a/crates/wit-parser/tests/ui/parse-fail/package-scope-use-world/root.wit b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-world/root.wit new file mode 100644 index 0000000000..faecc806a0 --- /dev/null +++ b/crates/wit-parser/tests/ui/parse-fail/package-scope-use-world/root.wit @@ -0,0 +1,7 @@ +package local:consumer; + +use local:types/point; + +interface api { + f: func(); +} diff --git a/crates/wit-parser/tests/ui/parse-fail/unresolved-interface3.wit.result b/crates/wit-parser/tests/ui/parse-fail/unresolved-interface3.wit.result index b8515dd20e..58f5b66d06 100644 --- a/crates/wit-parser/tests/ui/parse-fail/unresolved-interface3.wit.result +++ b/crates/wit-parser/tests/ui/parse-fail/unresolved-interface3.wit.result @@ -1,4 +1,4 @@ -interface or world `bar` does not exist +interface, world, or type `bar` does not exist --> tests/ui/parse-fail/unresolved-interface3.wit:5:5 | 5 | use bar as foo; 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 975def98c5..b4e428ddd9 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,4 +1,4 @@ -failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/use-world]: interface 'bar' not found in package 'foo:baz' +failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/use-world]: `bar` is a world; top-level use expects an interface or type --> tests/ui/parse-fail/use-world/root.wit:3:13 | 3 | use foo:baz/bar; diff --git a/crates/wit-parser/tests/ui/random.wit.json b/crates/wit-parser/tests/ui/random.wit.json index dcf2178c1c..c07aa9a6d3 100644 --- a/crates/wit-parser/tests/ui/random.wit.json +++ b/crates/wit-parser/tests/ui/random.wit.json @@ -50,7 +50,8 @@ "interfaces": { "random": 0 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/resources-empty.wit.json b/crates/wit-parser/tests/ui/resources-empty.wit.json index 4c16e4960e..16a76f206d 100644 --- a/crates/wit-parser/tests/ui/resources-empty.wit.json +++ b/crates/wit-parser/tests/ui/resources-empty.wit.json @@ -64,7 +64,8 @@ "interfaces": { "resources-empty": 0 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/resources-multiple-returns-own.wit.json b/crates/wit-parser/tests/ui/resources-multiple-returns-own.wit.json index f4d50ced19..276fd352a8 100644 --- a/crates/wit-parser/tests/ui/resources-multiple-returns-own.wit.json +++ b/crates/wit-parser/tests/ui/resources-multiple-returns-own.wit.json @@ -79,7 +79,8 @@ "interfaces": { "resources1": 0 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/resources-multiple.wit.json b/crates/wit-parser/tests/ui/resources-multiple.wit.json index 8b7969f2eb..c0006e595a 100644 --- a/crates/wit-parser/tests/ui/resources-multiple.wit.json +++ b/crates/wit-parser/tests/ui/resources-multiple.wit.json @@ -201,7 +201,8 @@ "interfaces": { "resources-multiple": 0 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/resources-return-own.wit.json b/crates/wit-parser/tests/ui/resources-return-own.wit.json index 290611e53b..e1efd1057b 100644 --- a/crates/wit-parser/tests/ui/resources-return-own.wit.json +++ b/crates/wit-parser/tests/ui/resources-return-own.wit.json @@ -67,7 +67,8 @@ "interfaces": { "resources1": 0 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/resources.wit.json b/crates/wit-parser/tests/ui/resources.wit.json index bf81cd6e4a..487d0e32b2 100644 --- a/crates/wit-parser/tests/ui/resources.wit.json +++ b/crates/wit-parser/tests/ui/resources.wit.json @@ -381,7 +381,8 @@ }, "worlds": { "w": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/resources1.wit.json b/crates/wit-parser/tests/ui/resources1.wit.json index 3bb1da4bbd..6144687e13 100644 --- a/crates/wit-parser/tests/ui/resources1.wit.json +++ b/crates/wit-parser/tests/ui/resources1.wit.json @@ -86,7 +86,8 @@ "interfaces": { "resources1": 0 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/same-name-import-export.wit.json b/crates/wit-parser/tests/ui/same-name-import-export.wit.json index ac0647ee14..873a652216 100644 --- a/crates/wit-parser/tests/ui/same-name-import-export.wit.json +++ b/crates/wit-parser/tests/ui/same-name-import-export.wit.json @@ -33,7 +33,8 @@ "interfaces": {}, "worlds": { "greeter": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/shared-types.wit.json b/crates/wit-parser/tests/ui/shared-types.wit.json index 8f53710b99..96dd598fb7 100644 --- a/crates/wit-parser/tests/ui/shared-types.wit.json +++ b/crates/wit-parser/tests/ui/shared-types.wit.json @@ -73,7 +73,8 @@ "interfaces": {}, "worlds": { "foo": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/simple-wasm-text.wit.json b/crates/wit-parser/tests/ui/simple-wasm-text.wit.json index 969c13a506..e5db39d27a 100644 --- a/crates/wit-parser/tests/ui/simple-wasm-text.wit.json +++ b/crates/wit-parser/tests/ui/simple-wasm-text.wit.json @@ -15,7 +15,8 @@ "interfaces": { "x": 0 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/since-and-unstable.wit.json b/crates/wit-parser/tests/ui/since-and-unstable.wit.json index 851694acba..cf12b4c8c5 100644 --- a/crates/wit-parser/tests/ui/since-and-unstable.wit.json +++ b/crates/wit-parser/tests/ui/since-and-unstable.wit.json @@ -602,7 +602,8 @@ "w1": 0, "w2": 1, "in-a-world": 2 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/streams-and-futures.wit.json b/crates/wit-parser/tests/ui/streams-and-futures.wit.json index 8a0b295308..75129cd366 100644 --- a/crates/wit-parser/tests/ui/streams-and-futures.wit.json +++ b/crates/wit-parser/tests/ui/streams-and-futures.wit.json @@ -199,7 +199,8 @@ "interfaces": { "streams-and-futures": 0 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/stress-export-elaborate.wit.json b/crates/wit-parser/tests/ui/stress-export-elaborate.wit.json index d77812776b..01300c7e1e 100644 --- a/crates/wit-parser/tests/ui/stress-export-elaborate.wit.json +++ b/crates/wit-parser/tests/ui/stress-export-elaborate.wit.json @@ -1150,7 +1150,8 @@ }, "worlds": { "foo": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/type-then-eof.wit.json b/crates/wit-parser/tests/ui/type-then-eof.wit.json index 54ecd5ed6d..7b1a694587 100644 --- a/crates/wit-parser/tests/ui/type-then-eof.wit.json +++ b/crates/wit-parser/tests/ui/type-then-eof.wit.json @@ -22,7 +22,8 @@ "interfaces": { "foo": 0 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/types.wit.json b/crates/wit-parser/tests/ui/types.wit.json index baa6a74849..0584effb73 100644 --- a/crates/wit-parser/tests/ui/types.wit.json +++ b/crates/wit-parser/tests/ui/types.wit.json @@ -762,7 +762,8 @@ "interfaces": { "types": 0 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/union-fuzz-1.wit.json b/crates/wit-parser/tests/ui/union-fuzz-1.wit.json index 31c7c82687..3b64b42619 100644 --- a/crates/wit-parser/tests/ui/union-fuzz-1.wit.json +++ b/crates/wit-parser/tests/ui/union-fuzz-1.wit.json @@ -29,7 +29,8 @@ "xo": 0, "name": 1, "x": 2 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/union-fuzz-2.wit.json b/crates/wit-parser/tests/ui/union-fuzz-2.wit.json index 36de6b23d5..587037fea0 100644 --- a/crates/wit-parser/tests/ui/union-fuzz-2.wit.json +++ b/crates/wit-parser/tests/ui/union-fuzz-2.wit.json @@ -56,7 +56,8 @@ "worlds": { "foo": 0, "bar": 1 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/unstable-resource.wit.json b/crates/wit-parser/tests/ui/unstable-resource.wit.json index 3d6677a486..32fd06d781 100644 --- a/crates/wit-parser/tests/ui/unstable-resource.wit.json +++ b/crates/wit-parser/tests/ui/unstable-resource.wit.json @@ -31,14 +31,16 @@ "interfaces": { "error": 0 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "a:b@0.1.0", "interfaces": { "foo": 1 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/use-chain.wit.json b/crates/wit-parser/tests/ui/use-chain.wit.json index bd942bb8cd..746bca8377 100644 --- a/crates/wit-parser/tests/ui/use-chain.wit.json +++ b/crates/wit-parser/tests/ui/use-chain.wit.json @@ -47,7 +47,8 @@ "foo": 0, "name": 1 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/use.wit.json b/crates/wit-parser/tests/ui/use.wit.json index 704bcdaaff..238d75a169 100644 --- a/crates/wit-parser/tests/ui/use.wit.json +++ b/crates/wit-parser/tests/ui/use.wit.json @@ -162,7 +162,8 @@ "use-multiple": 5, "trailing-comma": 6 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/version-syntax.wit.json b/crates/wit-parser/tests/ui/version-syntax.wit.json index a31d71534b..f5c71ca01c 100644 --- a/crates/wit-parser/tests/ui/version-syntax.wit.json +++ b/crates/wit-parser/tests/ui/version-syntax.wit.json @@ -6,52 +6,62 @@ { "name": "a:b@1.0.0-11-a", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "a:b@1.0.0-11ab", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "a:b@1.0.0-a1.1-a", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "a:b@1.0.0", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "a:b@1.0.1-1+1", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "a:b@1.0.1--", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "a:b@1.0.1-1a+1a", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "a:b@1.0.1-a+a", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "a:b@1.0.1", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:root", "interfaces": {}, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/versions.wit.json b/crates/wit-parser/tests/ui/versions.wit.json index febd55be51..8d1e55c7d3 100644 --- a/crates/wit-parser/tests/ui/versions.wit.json +++ b/crates/wit-parser/tests/ui/versions.wit.json @@ -71,21 +71,24 @@ "interfaces": { "foo": 0 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "a:a@2.0.0", "interfaces": { "foo": 1 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "foo:versions", "interfaces": { "foo": 2 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/wasi.wit.json b/crates/wit-parser/tests/ui/wasi.wit.json index e784fbd207..a33a8d8f95 100644 --- a/crates/wit-parser/tests/ui/wasi.wit.json +++ b/crates/wit-parser/tests/ui/wasi.wit.json @@ -533,7 +533,8 @@ "interfaces": { "wasi": 0 }, - "worlds": {} + "worlds": {}, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/with-resource-as.wit.json b/crates/wit-parser/tests/ui/with-resource-as.wit.json index 93cd6a4c90..4b35948330 100644 --- a/crates/wit-parser/tests/ui/with-resource-as.wit.json +++ b/crates/wit-parser/tests/ui/with-resource-as.wit.json @@ -215,7 +215,8 @@ "worlds": { "a": 0, "b": 1 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/world-diamond.wit.json b/crates/wit-parser/tests/ui/world-diamond.wit.json index 657c5b9c4a..cecf7a4fa4 100644 --- a/crates/wit-parser/tests/ui/world-diamond.wit.json +++ b/crates/wit-parser/tests/ui/world-diamond.wit.json @@ -109,7 +109,8 @@ }, "worlds": { "the-world": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/world-iface-no-collide.wit.json b/crates/wit-parser/tests/ui/world-iface-no-collide.wit.json index 4e86a4da8d..77469a38c0 100644 --- a/crates/wit-parser/tests/ui/world-iface-no-collide.wit.json +++ b/crates/wit-parser/tests/ui/world-iface-no-collide.wit.json @@ -61,7 +61,8 @@ }, "worlds": { "bar": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/world-implicit-import1.wit.json b/crates/wit-parser/tests/ui/world-implicit-import1.wit.json index 708c339b40..d903d58c82 100644 --- a/crates/wit-parser/tests/ui/world-implicit-import1.wit.json +++ b/crates/wit-parser/tests/ui/world-implicit-import1.wit.json @@ -75,7 +75,8 @@ }, "worlds": { "the-world": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/world-implicit-import2.wit.json b/crates/wit-parser/tests/ui/world-implicit-import2.wit.json index 25ab96a5b3..1617dee5eb 100644 --- a/crates/wit-parser/tests/ui/world-implicit-import2.wit.json +++ b/crates/wit-parser/tests/ui/world-implicit-import2.wit.json @@ -62,7 +62,8 @@ }, "worlds": { "w": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/world-implicit-import3.wit.json b/crates/wit-parser/tests/ui/world-implicit-import3.wit.json index 3036f69c74..54a4f74ad5 100644 --- a/crates/wit-parser/tests/ui/world-implicit-import3.wit.json +++ b/crates/wit-parser/tests/ui/world-implicit-import3.wit.json @@ -63,7 +63,8 @@ }, "worlds": { "w": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/world-same-fields4.wit.json b/crates/wit-parser/tests/ui/world-same-fields4.wit.json index 3be16acf9b..6ab2912fc1 100644 --- a/crates/wit-parser/tests/ui/world-same-fields4.wit.json +++ b/crates/wit-parser/tests/ui/world-same-fields4.wit.json @@ -76,7 +76,8 @@ }, "worlds": { "foo": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/world-top-level-funcs.wit.json b/crates/wit-parser/tests/ui/world-top-level-funcs.wit.json index df491f2349..93c41e8dd4 100644 --- a/crates/wit-parser/tests/ui/world-top-level-funcs.wit.json +++ b/crates/wit-parser/tests/ui/world-top-level-funcs.wit.json @@ -73,7 +73,8 @@ "interfaces": {}, "worlds": { "foo": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/world-top-level-resources.wit.json b/crates/wit-parser/tests/ui/world-top-level-resources.wit.json index 0babd96f9b..7d60c35987 100644 --- a/crates/wit-parser/tests/ui/world-top-level-resources.wit.json +++ b/crates/wit-parser/tests/ui/world-top-level-resources.wit.json @@ -219,7 +219,8 @@ }, "worlds": { "proxy": 0 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/worlds-union-dedup.wit.json b/crates/wit-parser/tests/ui/worlds-union-dedup.wit.json index d1e24e97b6..f8c4409f6b 100644 --- a/crates/wit-parser/tests/ui/worlds-union-dedup.wit.json +++ b/crates/wit-parser/tests/ui/worlds-union-dedup.wit.json @@ -106,7 +106,8 @@ "my-world-a": 0, "my-world-b": 1, "union-my-world": 2 - } + }, + "types": {} } ] } \ No newline at end of file diff --git a/crates/wit-parser/tests/ui/worlds-with-types.wit.json b/crates/wit-parser/tests/ui/worlds-with-types.wit.json index 19ccefac7d..ed3a5947b7 100644 --- a/crates/wit-parser/tests/ui/worlds-with-types.wit.json +++ b/crates/wit-parser/tests/ui/worlds-with-types.wit.json @@ -182,7 +182,8 @@ "foo": 0, "bar": 1, "the-test": 2 - } + }, + "types": {} } ] } \ No newline at end of file From 7e506c93f8333506cadf70d93fd45af797b134a8 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sat, 15 Aug 2026 01:13:38 -0400 Subject: [PATCH 02/12] Fix rustfmt for package-scope WIT types Signed-off-by: Yordis Prieto --- crates/wit-component/src/printing.rs | 6 +---- crates/wit-encoder/src/from_parser.rs | 9 ++++--- .../wit-encoder/tests/package-scope-types.rs | 24 +++++------------- crates/wit-parser/src/ast.rs | 4 +-- crates/wit-parser/src/ast/resolve.rs | 25 +++++++++---------- crates/wit-parser/src/decoding.rs | 15 +++-------- crates/wit-parser/src/resolve/mod.rs | 23 ++++++++--------- 7 files changed, 39 insertions(+), 67 deletions(-) diff --git a/crates/wit-component/src/printing.rs b/crates/wit-component/src/printing.rs index 64c7e97732..04e1cce1d0 100644 --- a/crates/wit-component/src/printing.rs +++ b/crates/wit-component/src/printing.rs @@ -120,11 +120,7 @@ impl WitPrinter { /// Emit toplevel `use ns:pkg/name;` for foreign package-scope types that /// are referenced from this package's interfaces/worlds. - fn print_foreign_package_type_uses( - &mut self, - resolve: &Resolve, - pkg: PackageId, - ) -> Result<()> { + fn print_foreign_package_type_uses(&mut self, resolve: &Resolve, pkg: PackageId) -> Result<()> { let mut live = LiveTypes::default(); for (_, &id) in resolve.packages[pkg].interfaces.iter() { live.add_interface(resolve, id); diff --git a/crates/wit-encoder/src/from_parser.rs b/crates/wit-encoder/src/from_parser.rs index 340e6df5bc..aef28d05d1 100644 --- a/crates/wit-encoder/src/from_parser.rs +++ b/crates/wit-encoder/src/from_parser.rs @@ -696,7 +696,10 @@ impl<'a> Converter<'a> { let type_def = self.resolve.types.get(*id).unwrap(); if let wit_parser::TypeOwner::Package(owner) = type_def.owner { if owner != package_id { - let name = type_def.name.as_deref().expect("package type must be named"); + let name = type_def + .name + .as_deref() + .expect("package type must be named"); let path = self.package_type_path(owner, name); if !uses.contains(&path) { uses.push(path); @@ -745,9 +748,7 @@ impl<'a> Converter<'a> { // only scan anonymous/inline interfaces owned here. if interface.package == Some(package_id) && interface.name.is_none() { self.collect_foreign_package_type_uses_from_interface( - package_id, - interface, - uses, + package_id, interface, uses, ); } } diff --git a/crates/wit-encoder/tests/package-scope-types.rs b/crates/wit-encoder/tests/package-scope-types.rs index 4d3213d59a..2a8906b004 100644 --- a/crates/wit-encoder/tests/package-scope-types.rs +++ b/crates/wit-encoder/tests/package-scope-types.rs @@ -126,12 +126,8 @@ world w { .iter() .find(|p| p.name().to_string() == "local:types") .unwrap(); - resolve2 - .push_str("types.wit", &types.to_string()) - .unwrap(); - resolve2 - .push_str("consumer.wit", &rendered) - .unwrap(); + resolve2.push_str("types.wit", &types.to_string()).unwrap(); + resolve2.push_str("consumer.wit", &rendered).unwrap(); let packages2 = packages_from_parsed(&resolve2); let rendered2 = packages2 .iter() @@ -231,12 +227,8 @@ world w { .iter() .find(|p| p.name().to_string() == "local:types") .unwrap(); - resolve2 - .push_str("types.wit", &types.to_string()) - .unwrap(); - resolve2 - .push_str("consumer.wit", &rendered) - .unwrap(); + resolve2.push_str("types.wit", &types.to_string()).unwrap(); + resolve2.push_str("consumer.wit", &rendered).unwrap(); let packages2 = packages_from_parsed(&resolve2); let rendered2 = packages2 .iter() @@ -296,12 +288,8 @@ world w { .iter() .find(|p| p.name().to_string() == "local:types") .unwrap(); - resolve2 - .push_str("types.wit", &types.to_string()) - .unwrap(); - resolve2 - .push_str("consumer.wit", &rendered) - .unwrap(); + resolve2.push_str("types.wit", &types.to_string()).unwrap(); + resolve2.push_str("consumer.wit", &rendered).unwrap(); let packages2 = packages_from_parsed(&resolve2); assert_eq!( consumer.to_string(), diff --git a/crates/wit-parser/src/ast.rs b/crates/wit-parser/src/ast.rs index 766b3be58b..ea8272865b 100644 --- a/crates/wit-parser/src/ast.rs +++ b/crates/wit-parser/src/ast.rs @@ -289,9 +289,7 @@ impl<'a> AstItem<'a> { Some((_span, Token::Package)) => { PackageFile::parse_nested(tokens, docs, attributes, depth).map(Self::Package) } - Some((_span, Token::Type)) => { - TypeDef::parse(tokens, docs, attributes).map(Self::Type) - } + Some((_span, Token::Type)) => TypeDef::parse(tokens, docs, attributes).map(Self::Type), Some((_span, Token::Flags)) => { TypeDef::parse_flags(tokens, docs, attributes).map(Self::Type) } diff --git a/crates/wit-parser/src/ast/resolve.rs b/crates/wit-parser/src/ast/resolve.rs index 082e4a53e8..b993d59914 100644 --- a/crates/wit-parser/src/ast/resolve.rs +++ b/crates/wit-parser/src/ast/resolve.rs @@ -575,9 +575,9 @@ impl<'a> Resolver<'a> { assert!(prev.is_none()); world_id_order.push(id); } - ast::AstItem::Type(_) - | ast::AstItem::Use(_) - | ast::AstItem::Package(_) => unreachable!(), + ast::AstItem::Type(_) | ast::AstItem::Use(_) | ast::AstItem::Package(_) => { + unreachable!() + } }; } Ok((iface_id_order, world_id_order, ids)) @@ -612,12 +612,14 @@ impl<'a> Resolver<'a> { continue; } None => { - return Err(ParseError::from(ParseErrorKind::ItemNotFound { - span: name.span, - name: name.name.to_string(), - kind: "interface, world, or type".to_owned(), - hint: None, - })); + return Err(ParseError::from( + ParseErrorKind::ItemNotFound { + span: name.span, + name: name.name.to_string(), + kind: "interface, world, or type".to_owned(), + hint: None, + }, + )); } } } @@ -1433,10 +1435,7 @@ impl<'a> Resolver<'a> { None if self.pending_package_type_names.contains(id.name) => { Err(ParseError::new_syntax( id.span, - format!( - "name `{}` is defined as a type, not an interface", - id.name - ), + format!("name `{}` is defined as a type, not an interface", id.name), )) } None => Err(ParseError::from(ParseErrorKind::ItemNotFound { diff --git a/crates/wit-parser/src/decoding.rs b/crates/wit-parser/src/decoding.rs index 82e3325171..1b4865b06c 100644 --- a/crates/wit-parser/src/decoding.rs +++ b/crates/wit-parser/src/decoding.rs @@ -242,8 +242,7 @@ impl ComponentInfo { ), } }; - let id = - decoder.register_type_export(&name, export, TypeOwner::None)?; + let id = decoder.register_type_export(&name, export, TypeOwner::None)?; let prev = fields.types.insert(name, id); if prev.is_some() { bail!("duplicate package-scope type export"); @@ -1675,11 +1674,7 @@ impl WitPackageDecoder<'_> { // Named package interfaces are handled above; still coalesce // anonymous ifaces reachable from worlds. if self.resolve.interfaces[iface_id].name.is_none() { - self.coalesce_package_types_in_interface( - iface_id, - &package_types, - &mut remap, - ); + self.coalesce_package_types_in_interface(iface_id, &package_types, &mut remap); } } } @@ -1697,11 +1692,7 @@ impl WitPackageDecoder<'_> { } } for (_id, world) in self.resolve.worlds.iter_mut() { - for item in world - .imports - .values_mut() - .chain(world.exports.values_mut()) - { + for item in world.imports.values_mut().chain(world.exports.values_mut()) { match item { WorldItem::Function(f) => rewrite_type_ids_in_func(f, &remap), WorldItem::Type { id, .. } => { diff --git a/crates/wit-parser/src/resolve/mod.rs b/crates/wit-parser/src/resolve/mod.rs index e21c9e5b48..f9f8be83d9 100644 --- a/crates/wit-parser/src/resolve/mod.rs +++ b/crates/wit-parser/src/resolve/mod.rs @@ -3867,9 +3867,7 @@ impl Remap { None if type_id.is_some() => { return Err(ResolveError::new_semantic( iface_span, - format!( - "name `{interface}` is defined as a type, not an interface" - ), + format!("name `{interface}` is defined as a type, not an interface"), )); } None => { @@ -3981,15 +3979,16 @@ impl Remap { let iface_id = self.map_interface(unresolved_iface_id, Default::default())?; let name = unresolved_ty.name.as_ref().unwrap(); let span = unresolved.unknown_type_spans[unresolved_type_id.index()]; - let type_id = *resolve.interfaces[iface_id] - .types - .get(name) - .ok_or_else(|| { - ResolveError::new_semantic( - span, - format!("type `{name}` not defined in interface"), - ) - })?; + let type_id = + *resolve.interfaces[iface_id] + .types + .get(name) + .ok_or_else(|| { + ResolveError::new_semantic( + span, + format!("type `{name}` not defined in interface"), + ) + })?; assert_eq!(self.types.len(), unresolved_type_id.index()); self.types.push(Some(type_id)); } From 4db4f5b17aef3a8258b9ab649a07eee4f30dca9c Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sat, 15 Aug 2026 01:26:08 -0400 Subject: [PATCH 03/12] Fix CI for package-scope WIT types Bless CLI nominal JSON expectations for Package.types, and drop the interfaces foreign-use fixture whose parse/decode prints cannot match under inline encoding. Signed-off-by: Yordis Prieto --- .../interfaces/package-scope-foreign-use.wat | 51 ------------------- .../package-scope-foreign-use/consumer.wit | 16 ------ .../consumer.wit.print | 20 -------- .../deps/types/types.wit | 10 ---- tests/cli/nominal1.wit.stdout | 3 +- tests/cli/nominal10.wit.stdout | 3 +- tests/cli/nominal11.wit.stdout | 3 +- tests/cli/nominal12.wit.stdout | 3 +- tests/cli/nominal13.wit.stdout | 3 +- tests/cli/nominal2.wit.stdout | 3 +- tests/cli/nominal3.wit.stdout | 3 +- tests/cli/nominal4.wit.stdout | 6 ++- tests/cli/nominal5.wit.stdout | 3 +- tests/cli/nominal6.wit.stdout | 3 +- tests/cli/nominal7.wit.stdout | 3 +- tests/cli/nominal8.wit.stdout | 3 +- tests/cli/nominal9.wit.stdout | 3 +- 17 files changed, 28 insertions(+), 111 deletions(-) delete mode 100644 crates/wit-component/tests/interfaces/package-scope-foreign-use.wat delete mode 100644 crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit delete mode 100644 crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit.print delete mode 100644 crates/wit-component/tests/interfaces/package-scope-foreign-use/deps/types/types.wit diff --git a/crates/wit-component/tests/interfaces/package-scope-foreign-use.wat b/crates/wit-component/tests/interfaces/package-scope-foreign-use.wat deleted file mode 100644 index ffb2390b66..0000000000 --- a/crates/wit-component/tests/interfaces/package-scope-foreign-use.wat +++ /dev/null @@ -1,51 +0,0 @@ -(component - (type (;0;) (record (field "x" u32) (field "y" u32))) - (export (;1;) "point" (type 0)) - (type (;2;) (record (field "p" 1))) - (export (;3;) "bin" (type 2)) - (type (;4;) - (component - (type (;0;) - (instance - (type (;0;) (record (field "x" u32) (field "y" u32))) - (export (;1;) "point" (type (eq 0))) - (type (;2;) (record (field "p" 1))) - (export (;3;) "bin" (type (eq 2))) - (type (;4;) (func (param "b" 3))) - (export (;0;) "wrap" (func (type 4))) - (type (;5;) (func (param "p" 1))) - (export (;1;) "move-to" (func (type 5))) - ) - ) - (export (;0;) "local:consumer/api" (instance (type 0))) - ) - ) - (export (;5;) "api" (type 4)) - (type (;6;) - (component - (type (;0;) - (component - (type (;0;) - (instance - (type (;0;) (record (field "x" u32) (field "y" u32))) - (export (;1;) "point" (type (eq 0))) - (type (;2;) (record (field "p" 1))) - (export (;3;) "bin" (type (eq 2))) - (type (;4;) (func (param "b" 3))) - (export (;0;) "wrap" (func (type 4))) - (type (;5;) (func (param "p" 1))) - (export (;1;) "move-to" (func (type 5))) - ) - ) - (export (;0;) "local:consumer/api" (instance (type 0))) - ) - ) - (export (;0;) "local:consumer/w" (component (type 0))) - ) - ) - (export (;7;) "w" (type 6)) - (@custom "package-docs" "\01{}") - (@producers - (processed-by "wit-component" "$CARGO_PKG_VERSION") - ) -) diff --git a/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit b/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit deleted file mode 100644 index a1703d92f3..0000000000 --- a/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit +++ /dev/null @@ -1,16 +0,0 @@ -package local:consumer; - -use local:types/point; - -record bin { - p: point, -} - -interface api { - wrap: func(b: bin); - move-to: func(p: point); -} - -world w { - export api; -} diff --git a/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit.print b/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit.print deleted file mode 100644 index b27ded5342..0000000000 --- a/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit.print +++ /dev/null @@ -1,20 +0,0 @@ -package local:consumer; - -record point { - x: u32, - y: u32, -} - -record bin { - p: point, -} - -interface api { - wrap: func(b: bin); - - move-to: func(p: point); -} - -world w { - export api; -} diff --git a/crates/wit-component/tests/interfaces/package-scope-foreign-use/deps/types/types.wit b/crates/wit-component/tests/interfaces/package-scope-foreign-use/deps/types/types.wit deleted file mode 100644 index d25c1fd54a..0000000000 --- a/crates/wit-component/tests/interfaces/package-scope-foreign-use/deps/types/types.wit +++ /dev/null @@ -1,10 +0,0 @@ -package local:types; - -/// Shared package-scope point. -record point { - x: u32, - y: u32, -} - -interface unused { -} diff --git a/tests/cli/nominal1.wit.stdout b/tests/cli/nominal1.wit.stdout index b9ff3e7604..1205589a10 100644 --- a/tests/cli/nominal1.wit.stdout +++ b/tests/cli/nominal1.wit.stdout @@ -97,7 +97,8 @@ }, "worlds": { "foo": 0 - } + }, + "types": {} } ] } diff --git a/tests/cli/nominal10.wit.stdout b/tests/cli/nominal10.wit.stdout index 6aa93710b1..232e214ca6 100644 --- a/tests/cli/nominal10.wit.stdout +++ b/tests/cli/nominal10.wit.stdout @@ -91,7 +91,8 @@ }, "worlds": { "foo": 0 - } + }, + "types": {} } ] } diff --git a/tests/cli/nominal11.wit.stdout b/tests/cli/nominal11.wit.stdout index da7aeb39ad..ce4da45535 100644 --- a/tests/cli/nominal11.wit.stdout +++ b/tests/cli/nominal11.wit.stdout @@ -138,7 +138,8 @@ }, "worlds": { "foo": 0 - } + }, + "types": {} } ] } diff --git a/tests/cli/nominal12.wit.stdout b/tests/cli/nominal12.wit.stdout index 7e2717a5f5..8077ec6854 100644 --- a/tests/cli/nominal12.wit.stdout +++ b/tests/cli/nominal12.wit.stdout @@ -161,7 +161,8 @@ }, "worlds": { "foo": 0 - } + }, + "types": {} } ] } diff --git a/tests/cli/nominal13.wit.stdout b/tests/cli/nominal13.wit.stdout index 600535dc1e..fa90342b23 100644 --- a/tests/cli/nominal13.wit.stdout +++ b/tests/cli/nominal13.wit.stdout @@ -58,7 +58,8 @@ }, "worlds": { "w": 0 - } + }, + "types": {} } ] } diff --git a/tests/cli/nominal2.wit.stdout b/tests/cli/nominal2.wit.stdout index 01060209ce..5d7775f3da 100644 --- a/tests/cli/nominal2.wit.stdout +++ b/tests/cli/nominal2.wit.stdout @@ -137,7 +137,8 @@ }, "worlds": { "foo": 0 - } + }, + "types": {} } ] } diff --git a/tests/cli/nominal3.wit.stdout b/tests/cli/nominal3.wit.stdout index 15aaca488d..bf3b433645 100644 --- a/tests/cli/nominal3.wit.stdout +++ b/tests/cli/nominal3.wit.stdout @@ -97,7 +97,8 @@ }, "worlds": { "foo": 0 - } + }, + "types": {} } ] } diff --git a/tests/cli/nominal4.wit.stdout b/tests/cli/nominal4.wit.stdout index f2a0db18fd..20cbf4754b 100644 --- a/tests/cli/nominal4.wit.stdout +++ b/tests/cli/nominal4.wit.stdout @@ -92,7 +92,8 @@ "interfaces": { "x": 0 }, - "worlds": {} + "worlds": {}, + "types": {} }, { "name": "a:b", @@ -102,7 +103,8 @@ "interfaces": {}, "worlds": { "foo": 0 - } + }, + "types": {} } ] } diff --git a/tests/cli/nominal5.wit.stdout b/tests/cli/nominal5.wit.stdout index fc7f0bd6cc..e63fdcd915 100644 --- a/tests/cli/nominal5.wit.stdout +++ b/tests/cli/nominal5.wit.stdout @@ -57,7 +57,8 @@ }, "worlds": { "foo": 0 - } + }, + "types": {} } ] } diff --git a/tests/cli/nominal6.wit.stdout b/tests/cli/nominal6.wit.stdout index 82c375a627..4bbbcb93f0 100644 --- a/tests/cli/nominal6.wit.stdout +++ b/tests/cli/nominal6.wit.stdout @@ -58,7 +58,8 @@ }, "worlds": { "foo": 0 - } + }, + "types": {} } ] } diff --git a/tests/cli/nominal7.wit.stdout b/tests/cli/nominal7.wit.stdout index 6ab64be37f..84eb2a87e4 100644 --- a/tests/cli/nominal7.wit.stdout +++ b/tests/cli/nominal7.wit.stdout @@ -58,7 +58,8 @@ }, "worlds": { "foo": 0 - } + }, + "types": {} } ] } diff --git a/tests/cli/nominal8.wit.stdout b/tests/cli/nominal8.wit.stdout index e20f038f9d..55489cf8ef 100644 --- a/tests/cli/nominal8.wit.stdout +++ b/tests/cli/nominal8.wit.stdout @@ -58,7 +58,8 @@ }, "worlds": { "foo": 0 - } + }, + "types": {} } ] } diff --git a/tests/cli/nominal9.wit.stdout b/tests/cli/nominal9.wit.stdout index 2a84af6a9d..a7ebd09862 100644 --- a/tests/cli/nominal9.wit.stdout +++ b/tests/cli/nominal9.wit.stdout @@ -57,7 +57,8 @@ }, "worlds": { "foo": 0 - } + }, + "types": {} } ] } From c25f3630ca3b4a8185c102709fbce8247549824e Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 18 Aug 2026 13:07:36 -0400 Subject: [PATCH 04/12] Encode package-scope types as fully-qualified `eq` imports Align the encoding with the approved package-scope types design in WebAssembly/component-model#699. Instead of inlining a package-scope type into every interface/world instance and re-exporting it, a package-scope type is now encoded like a `use` of an interface without the wrapping instance: the definition is restated and bound with an `eq` import named by its fully-qualified `namespace:package/name`, so nothing has to be supplied to satisfy it. The referencing interface or world aliases that import into its instance/component type. A package-scope type that depends on one from another package puts the `import` on the package's own component, since there is no wrapping component-type to hold it there. Decoding recognizes those qualified type imports as package-scope dependencies and keeps ownership with the defining package, dropping the previous inline-coalescing pass. The printer emits the corresponding top-level `use` before the definitions that reference it. This makes the encoded output valid per Component Model validation and round-trips through decode/encode. Add golden fixtures mirroring the approved WIT.md examples plus unit tests covering foreign use, foreign dependency, versioned names, the export-before-use ordering, the resource rejection, and that `component new` exports the interface directly without inventing a types interface. Signed-off-by: Yordis Prieto --- crates/wit-component/src/encoding/types.rs | 165 ++++---- crates/wit-component/src/encoding/wit.rs | 201 +++++++++- crates/wit-component/src/lib.rs | 302 ++++++++++++++- crates/wit-component/src/printing.rs | 9 +- .../tests/interfaces/package-scope-docs.wat | 26 +- .../interfaces/package-scope-field-docs.wat | 62 +-- .../interfaces/package-scope-foreign-use.wat | 55 +++ .../package-scope-foreign-use/consumer.wit | 16 + .../consumer.wit.print | 17 + .../deps/types/types.wit | 10 + .../tests/interfaces/package-scope-nested.wat | 26 +- .../package-scope-spec-interface.wat | 23 ++ .../package-scope-spec-interface.wit | 7 + .../package-scope-spec-interface.wit.print | 11 + .../interfaces/package-scope-spec-world.wat | 22 ++ .../interfaces/package-scope-spec-world.wit | 7 + .../package-scope-spec-world.wit.print | 10 + .../tests/interfaces/package-scope-types.wat | 97 ++--- .../interfaces/package-scope-world-only.wat | 2 +- crates/wit-parser/src/decoding.rs | 362 ++++++++---------- 20 files changed, 1039 insertions(+), 391 deletions(-) create mode 100644 crates/wit-component/tests/interfaces/package-scope-foreign-use.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit.print create mode 100644 crates/wit-component/tests/interfaces/package-scope-foreign-use/deps/types/types.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-spec-interface.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-spec-interface.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-spec-interface.wit.print create mode 100644 crates/wit-component/tests/interfaces/package-scope-spec-world.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-spec-world.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-spec-world.wit.print diff --git a/crates/wit-component/src/encoding/types.rs b/crates/wit-component/src/encoding/types.rs index 6b43e5a84e..74cb2c209b 100644 --- a/crates/wit-component/src/encoding/types.rs +++ b/crates/wit-component/src/encoding/types.rs @@ -74,7 +74,7 @@ impl<'a> TypeEncodingMaps<'a> { } /// Inserts that `id` was encoded as `index`. - fn insert(&mut self, resolve: &'a Resolve, id: TypeId, index: u32) { + pub(crate) fn insert(&mut self, resolve: &'a Resolve, id: TypeId, index: u32) { // Always record the id=>index mapping. self.id_to_index.insert(id, index); @@ -121,6 +121,15 @@ pub trait ValtypeEncoder<'a> { /// imported type into this index space. fn import_type(&mut self, interface: InterfaceId, id: TypeId) -> u32; + /// Imports the package-scope type `id`, returning the index of the imported + /// type into this index space. + /// + /// Returns `None` when this encoder defines package-scope types itself + /// rather than depending on them. + fn import_package_type(&mut self, _resolve: &'a Resolve, _id: TypeId) -> Result> { + Ok(None) + } + /// Returns the identifier of the interface that generation is for. fn interface(&self) -> Option; @@ -193,72 +202,27 @@ pub trait ValtypeEncoder<'a> { let ty = &resolve.types[id]; - // If this type is imported from another interface then return - // it as it was bound here with an alias. + // If this type is imported from another interface, or from + // package scope, then return it as it was bound here with an + // alias. log::trace!("encode type name={:?} {:?}", ty.name, &ty.kind); - if let Some(index) = self.maybe_import_type(resolve, id) { + if let Some(index) = self.maybe_import_type(resolve, id)? { self.type_encoding_maps().insert(resolve, id, index); return Ok(ComponentValType::Type(index)); } + // Resources are named by the export that creates them rather + // than by a structural definition, so they don't participate in + // the define-then-name dance below. + if let TypeDefKind::Resource = &ty.kind { + let name = ty.name.as_ref().expect("resources must be named"); + let index = self.export_resource(extern_name(name, ty.external_id.as_deref())); + self.type_encoding_maps().id_to_index.insert(id, index); + return Ok(ComponentValType::Type(index)); + } + // ... and failing all that insert the type export. - let mut encoded = match &ty.kind { - TypeDefKind::Record(r) => self.encode_record(resolve, r)?, - TypeDefKind::Tuple(t) => self.encode_tuple(resolve, t)?, - TypeDefKind::Flags(r) => self.encode_flags(r)?, - TypeDefKind::Variant(v) => self.encode_variant(resolve, v)?, - TypeDefKind::Option(t) => self.encode_option(resolve, t)?, - TypeDefKind::Result(r) => self.encode_result(resolve, r)?, - TypeDefKind::Enum(e) => self.encode_enum(e)?, - TypeDefKind::List(ty) => { - let ty = self.encode_valtype(resolve, ty)?; - let (index, encoder) = self.defined_type(); - encoder.list(ty); - ComponentValType::Type(index) - } - TypeDefKind::Map(key_ty, value_ty) => { - let key = self.encode_valtype(resolve, key_ty)?; - let value = self.encode_valtype(resolve, value_ty)?; - let (index, encoder) = self.defined_type(); - encoder.map(key, value); - ComponentValType::Type(index) - } - TypeDefKind::FixedLengthList(ty, elements) => { - let ty = self.encode_valtype(resolve, ty)?; - let (index, encoder) = self.defined_type(); - encoder.fixed_length_list(ty, *elements); - ComponentValType::Type(index) - } - TypeDefKind::Type(ty) => self.encode_valtype(resolve, ty)?, - TypeDefKind::Future(ty) => self.encode_future(resolve, ty)?, - TypeDefKind::Stream(ty) => self.encode_stream(resolve, ty)?, - TypeDefKind::Unknown => unreachable!(), - TypeDefKind::Resource => { - let name = ty.name.as_ref().expect("resources must be named"); - let index = - self.export_resource(extern_name(name, ty.external_id.as_deref())); - self.type_encoding_maps().id_to_index.insert(id, index); - return Ok(ComponentValType::Type(index)); - } - TypeDefKind::Handle(Handle::Own(id)) => { - let ty = match self.encode_valtype(resolve, &Type::Id(*id))? { - ComponentValType::Type(index) => index, - _ => panic!("must be an indexed type"), - }; - let (index, encoder) = self.defined_type(); - encoder.own(ty); - ComponentValType::Type(index) - } - TypeDefKind::Handle(Handle::Borrow(id)) => { - let ty = match self.encode_valtype(resolve, &Type::Id(*id))? { - ComponentValType::Type(index) => index, - _ => panic!("must be an indexed type"), - }; - let (index, encoder) = self.defined_type(); - encoder.borrow(ty); - ComponentValType::Type(index) - } - }; + let mut encoded = self.encode_typedef_structure(resolve, id)?; if let Some(name) = &ty.name { let index = match encoded { @@ -287,20 +251,83 @@ pub trait ValtypeEncoder<'a> { }) } - /// Optionally imports `id` from a different interface, returning the index - /// of the imported type into this index space. + /// Encodes the structure of `id`'s definition into this index space. + /// + /// Unlike [`ValtypeEncoder::encode_valtype`] this never names the result, so + /// it's suitable for restating a definition that is going to be named some + /// other way, such as by the `import` that binds a package-scope type. + fn encode_typedef_structure( + &mut self, + resolve: &'a Resolve, + id: TypeId, + ) -> Result { + Ok(match &resolve.types[id].kind { + TypeDefKind::Record(r) => self.encode_record(resolve, r)?, + TypeDefKind::Tuple(t) => self.encode_tuple(resolve, t)?, + TypeDefKind::Flags(r) => self.encode_flags(r)?, + TypeDefKind::Variant(v) => self.encode_variant(resolve, v)?, + TypeDefKind::Option(t) => self.encode_option(resolve, t)?, + TypeDefKind::Result(r) => self.encode_result(resolve, r)?, + TypeDefKind::Enum(e) => self.encode_enum(e)?, + TypeDefKind::List(ty) => { + let ty = self.encode_valtype(resolve, ty)?; + let (index, encoder) = self.defined_type(); + encoder.list(ty); + ComponentValType::Type(index) + } + TypeDefKind::Map(key_ty, value_ty) => { + let key = self.encode_valtype(resolve, key_ty)?; + let value = self.encode_valtype(resolve, value_ty)?; + let (index, encoder) = self.defined_type(); + encoder.map(key, value); + ComponentValType::Type(index) + } + TypeDefKind::FixedLengthList(ty, elements) => { + let ty = self.encode_valtype(resolve, ty)?; + let (index, encoder) = self.defined_type(); + encoder.fixed_length_list(ty, *elements); + ComponentValType::Type(index) + } + TypeDefKind::Type(ty) => self.encode_valtype(resolve, ty)?, + TypeDefKind::Future(ty) => self.encode_future(resolve, ty)?, + TypeDefKind::Stream(ty) => self.encode_stream(resolve, ty)?, + TypeDefKind::Handle(Handle::Own(id)) => { + let ty = match self.encode_valtype(resolve, &Type::Id(*id))? { + ComponentValType::Type(index) => index, + _ => panic!("must be an indexed type"), + }; + let (index, encoder) = self.defined_type(); + encoder.own(ty); + ComponentValType::Type(index) + } + TypeDefKind::Handle(Handle::Borrow(id)) => { + let ty = match self.encode_valtype(resolve, &Type::Id(*id))? { + ComponentValType::Type(index) => index, + _ => panic!("must be an indexed type"), + }; + let (index, encoder) = self.defined_type(); + encoder.borrow(ty); + ComponentValType::Type(index) + } + TypeDefKind::Resource => unreachable!("resources have no structure to encode"), + TypeDefKind::Unknown => unreachable!(), + }) + } + + /// Optionally imports `id` from a different interface or from package scope, + /// returning the index of the imported type into this index space. /// /// Returns `None` if `id` can't be imported. - fn maybe_import_type(&mut self, resolve: &Resolve, id: TypeId) -> Option { - let ty = &resolve.types[id]; - let owner = match ty.owner { + fn maybe_import_type(&mut self, resolve: &'a Resolve, id: TypeId) -> Result> { + let owner = match resolve.types[id].owner { TypeOwner::Interface(i) => i, - _ => return None, + TypeOwner::Package(_) => return self.import_package_type(resolve, id), + TypeOwner::World(_) | TypeOwner::None => return Ok(None), }; if Some(owner) == self.interface() { - return None; + return Ok(None); } - Some(self.import_type(owner, id)) + Ok(Some(self.import_type(owner, id))) } fn encode_optional_valtype( diff --git a/crates/wit-component/src/encoding/wit.rs b/crates/wit-component/src/encoding/wit.rs index 9a4a84ef5f..eec548508a 100644 --- a/crates/wit-component/src/encoding/wit.rs +++ b/crates/wit-component/src/encoding/wit.rs @@ -82,12 +82,7 @@ pub fn encode_world(resolve: &Resolve, world_id: WorldId) -> Result { component.interface = None; - // Package-scope types referenced by freestanding world funcs - // must be imported into the world component type (same as - // world-owned named types), not exported. - component.import_types = true; let idx = component.encode_func_type(resolve, f)?; - component.import_types = false; ComponentTypeRef::Func(idx) } WorldItem::Type { id, .. } => { @@ -113,12 +108,7 @@ pub fn encode_world(resolve: &Resolve, world_id: WorldId) -> Result { component.interface = None; - // Package-scope types referenced by freestanding world funcs - // must be imported into the world component type (same as - // world-owned named types), not exported. - component.import_types = true; let idx = component.encode_func_type(resolve, f)?; - component.import_types = false; ComponentTypeRef::Func(idx) } WorldItem::Type { .. } => unreachable!(), @@ -157,7 +147,10 @@ impl Encoder<'_> { { let mut encoder = PackageTypeEncoder { component: &mut self.component, + package: self.package, type_encoding_maps: Default::default(), + foreign_type_maps: Default::default(), + foreign_declared: Default::default(), }; for (_, &id) in self.resolve.packages[self.package].types.iter() { encoder.encode_valtype(self.resolve, &Type::Id(id))?; @@ -262,6 +255,10 @@ struct InterfaceEncoder<'a> { ty: Option, type_encoding_maps: TypeEncodingMaps<'a>, saved_maps: Option>, + /// Encoding maps for the definitions restated in `outer` to bind + /// package-scope type imports, kept separate from the maps of whichever + /// scope is currently being encoded. + package_type_maps: TypeEncodingMaps<'a>, import_map: HashMap, outer_type_map: HashMap, instances: u32, @@ -269,13 +266,14 @@ struct InterfaceEncoder<'a> { interface: Option, } -impl InterfaceEncoder<'_> { +impl<'a> InterfaceEncoder<'a> { fn new(resolve: &Resolve) -> InterfaceEncoder<'_> { InterfaceEncoder { resolve, outer: ComponentType::new(), ty: None, type_encoding_maps: Default::default(), + package_type_maps: Default::default(), import_map: Default::default(), outer_type_map: Default::default(), instances: 0, @@ -285,6 +283,18 @@ impl InterfaceEncoder<'_> { } } + /// Declares the package-scope type `id` in `outer`, returning the index of + /// the `import` that binds it there. + fn declare_package_type(&mut self, resolve: &'a Resolve, id: TypeId) -> Result { + PackageTypeImporter { + resolve, + outer: &mut self.outer, + type_encoding_maps: &mut self.package_type_maps, + declared: &mut self.outer_type_map, + } + .declare(id) + } + fn encode_instance(&mut self, interface: InterfaceId) -> Result { self.push_instance(); let iface = &self.resolve.interfaces[interface]; @@ -414,6 +424,18 @@ impl<'a> ValtypeEncoder<'a> for InterfaceEncoder<'a> { }); ret }); + self.alias_outer_type(outer_idx) + } + fn import_package_type(&mut self, resolve: &'a Resolve, id: TypeId) -> Result> { + let outer_idx = self.declare_package_type(resolve, id)?; + Ok(Some(self.alias_outer_type(outer_idx))) + } +} + +impl InterfaceEncoder<'_> { + /// Brings `outer_idx`, an index in the wrapping component-type, into the + /// instance type currently being built, if any. + fn alias_outer_type(&mut self, outer_idx: u32) -> u32 { match &mut self.ty { Some(ty) => { let ret = ty.type_count(); @@ -429,12 +451,144 @@ impl<'a> ValtypeEncoder<'a> for InterfaceEncoder<'a> { } } -struct PackageTypeEncoder<'a> { - component: &'a mut ComponentBuilder, +/// An index space that a restated package-scope type definition, and the +/// `import` naming it, can be written into. +trait PackageTypeScope { + fn defined_type(&mut self) -> (u32, ComponentDefinedTypeEncoder<'_>); + fn define_function_type(&mut self) -> (u32, ComponentFuncTypeEncoder<'_>); + /// Imports a type bound with `eq` to `index`, returning the index of the + /// imported type. + fn import_eq(&mut self, name: ComponentExternName<'_>, index: u32) -> u32; +} + +impl PackageTypeScope for ComponentType { + fn defined_type(&mut self) -> (u32, ComponentDefinedTypeEncoder<'_>) { + (self.type_count(), self.ty().defined_type()) + } + fn define_function_type(&mut self) -> (u32, ComponentFuncTypeEncoder<'_>) { + (self.type_count(), self.ty().function()) + } + fn import_eq(&mut self, name: ComponentExternName<'_>, index: u32) -> u32 { + let ret = self.type_count(); + self.import(name, ComponentTypeRef::Type(TypeBounds::Eq(index))); + ret + } +} + +impl PackageTypeScope for ComponentBuilder { + fn defined_type(&mut self) -> (u32, ComponentDefinedTypeEncoder<'_>) { + self.type_defined(None) + } + fn define_function_type(&mut self) -> (u32, ComponentFuncTypeEncoder<'_>) { + self.type_function(None) + } + fn import_eq(&mut self, name: ComponentExternName<'_>, index: u32) -> u32 { + self.import(name, ComponentTypeRef::Type(TypeBounds::Eq(index))) + } +} + +/// Declares package-scope types that something depends on. +/// +/// A package-scope type is encoded like a `use` of an `interface`, just without +/// the wrapping `instance` type: the definition is restated locally and then +/// imported under its fully-qualified `namespace:package/name`, bound with `eq` +/// so that nothing has to be supplied to satisfy the import. +struct PackageTypeImporter<'b, 'a, S> { + resolve: &'a Resolve, + outer: &'b mut S, + type_encoding_maps: &'b mut TypeEncodingMaps<'a>, + declared: &'b mut HashMap, +} + +impl<'a, S: PackageTypeScope> PackageTypeImporter<'_, 'a, S> { + fn declare(&mut self, id: TypeId) -> Result { + if let Some(index) = self.declared.get(&id) { + return Ok(*index); + } + + let resolve = self.resolve; + let structure = match self.encode_typedef_structure(resolve, id)? { + ComponentValType::Type(index) => index, + // A named primitive still needs an entry in the type section for + // the import's bound to refer to. + ComponentValType::Primitive(ty) => { + let (index, encoder) = self.defined_type(); + encoder.primitive(ty); + index + } + }; + + let ty = &resolve.types[id]; + let index = self.outer.import_eq( + ComponentExternName { + name: package_type_name(resolve, id).into(), + implements: None, + version_suffix: None, + external_id: ty.external_id.clone().map(Into::into), + }, + structure, + ); + + self.declared.insert(id, index); + self.type_encoding_maps.insert(resolve, id, index); + Ok(index) + } +} + +impl<'a, S: PackageTypeScope> ValtypeEncoder<'a> for PackageTypeImporter<'_, 'a, S> { + fn defined_type(&mut self) -> (u32, ComponentDefinedTypeEncoder<'_>) { + self.outer.defined_type() + } + fn define_function_type(&mut self) -> (u32, ComponentFuncTypeEncoder<'_>) { + self.outer.define_function_type() + } + fn export_type(&mut self, _index: u32, _name: ComponentExternName<'a>) -> Option { + // Definitions restated here are named by the `import` that binds them, + // so the index of the definition itself is what callers want. + None + } + fn export_resource(&mut self, _name: ComponentExternName<'a>) -> u32 { + unreachable!("package-scope resources are not allowed") + } + fn type_encoding_maps(&mut self) -> &mut TypeEncodingMaps<'a> { + self.type_encoding_maps + } + fn interface(&self) -> Option { + None + } + fn import_type(&mut self, _owner: InterfaceId, _id: TypeId) -> u32 { + unreachable!("package-scope types cannot refer to interface types") + } + fn import_package_type(&mut self, _resolve: &'a Resolve, id: TypeId) -> Result> { + self.declare(id).map(Some) + } +} + +/// The fully-qualified `namespace:package/name` of a package-scope type. +fn package_type_name(resolve: &Resolve, id: TypeId) -> String { + let ty = &resolve.types[id]; + let package = match ty.owner { + TypeOwner::Package(package) => package, + _ => unreachable!("not a package-scope type"), + }; + let name = ty.name.as_ref().expect("package-scope types are named"); + resolve.packages[package].name.interface_id(name) +} + +/// Encodes a package's own package-scope types as type exports of the +/// package's component. +struct PackageTypeEncoder<'b, 'a> { + component: &'b mut ComponentBuilder, + package: PackageId, type_encoding_maps: TypeEncodingMaps<'a>, + /// Encoding maps for the definitions restated to bind imports of + /// package-scope types from other packages, kept separate from the maps of + /// the definitions this package exports. + foreign_type_maps: TypeEncodingMaps<'a>, + foreign_declared: HashMap, } -impl<'a> ValtypeEncoder<'a> for PackageTypeEncoder<'a> { +impl<'a> ValtypeEncoder<'a> for PackageTypeEncoder<'_, 'a> { fn defined_type(&mut self) -> (u32, ComponentDefinedTypeEncoder<'_>) { self.component.type_defined(None) } @@ -459,4 +613,23 @@ impl<'a> ValtypeEncoder<'a> for PackageTypeEncoder<'a> { fn import_type(&mut self, _owner: InterfaceId, _id: TypeId) -> u32 { unreachable!("package-scope types do not import from interfaces") } + fn import_package_type(&mut self, resolve: &'a Resolve, id: TypeId) -> Result> { + // This encoder is defining the package's own types, so those are + // defined and exported rather than imported. + if resolve.types[id].owner == TypeOwner::Package(self.package) { + return Ok(None); + } + + // A definition from another package has no wrapping component-type to + // hold the `import` naming it, so it goes on the package's component + // directly. + PackageTypeImporter { + resolve, + outer: &mut *self.component, + type_encoding_maps: &mut self.foreign_type_maps, + declared: &mut self.foreign_declared, + } + .declare(id) + .map(Some) + } } diff --git a/crates/wit-component/src/lib.rs b/crates/wit-component/src/lib.rs index 1f7e80b10b..907c511423 100644 --- a/crates/wit-component/src/lib.rs +++ b/crates/wit-component/src/lib.rs @@ -177,7 +177,7 @@ world test-world {} } #[test] - fn package_scope_foreign_type_encodes_without_import() -> Result<()> { + fn package_scope_foreign_type_encodes_as_qualified_import() -> Result<()> { let mut resolve = Resolve::new(); resolve.push_str( "types.wit", @@ -211,13 +211,19 @@ world w { let wasm = encode(&resolve, pkg)?; let wat = wasmprinter::print_bytes(&wasm)?; + // The definition is restated locally and the import names where it came + // from, just as it is for a type projected out of a foreign interface. assert!( - !wat.contains("(import"), - "package-scope foreign type must be inlined, not imported:\n{wat}" + wat.contains("(record (field \"x\" u32) (field \"y\" u32))"), + "expected the foreign definition to be restated:\n{wat}" ); assert!( - wat.contains("(record"), - "expected inlined record for foreign package type:\n{wat}" + wat.contains("(import \"local:types/point\" (type"), + "expected a qualified import for the foreign package type:\n{wat}" + ); + assert!( + !wat.contains("(import \"local:types/point\" (instance"), + "a package-scope type must not be wrapped in an instance:\n{wat}" ); wasmparser::Validator::new_with_features(WasmFeatures::all()).validate_all(&wasm)?; @@ -322,7 +328,10 @@ package local:nested { wat.contains("(export (;1;) \"point\" (type 0))"), "expected package-scope point export:\n{wat}" ); - assert!(!wat.contains("(import"), "{wat}"); + assert!( + wat.contains("(import \"local:nested/point\" (type"), + "expected the interface to import the package-scope type:\n{wat}" + ); wasmparser::Validator::new_with_features(WasmFeatures::all()).validate_all(&wasm)?; let decoded = crate::decode(&wasm)?; @@ -335,6 +344,44 @@ package local:nested { Ok(()) } + /// The approved WIT.md "a package-scope type may refer to another one" + /// example: since an exported type may only refer to types named by an + /// import or export, the referenced definition is exported under its own id + /// first and the dependent one refers back to that export. + #[test] + fn package_scope_type_referring_to_another_exports_before_use() -> Result<()> { + let mut resolve = Resolve::new(); + let pkg = resolve.push_str( + "demo.wit", + r#" +package local:demo; + +record point { + x: u32, + y: u32, +} + +type point-list = list; +"#, + )?; + + let wasm = encode(&resolve, pkg)?; + let wat = wasmprinter::print_bytes(&wasm)?; + // `point` is exported first as a record... + assert!( + wat.contains("(record (field \"x\" u32) (field \"y\" u32))") + && wat.contains("\"point\" (type 0)"), + "expected `point` to be exported as its own definition:\n{wat}" + ); + // ...and `point-list` is a `list` of that exported type, exported next. + assert!( + wat.contains("(type (;2;) (list 1))") && wat.contains("\"point-list\" (type 2)"), + "expected `point-list` to reference the exported `point`:\n{wat}" + ); + wasmparser::Validator::new_with_features(WasmFeatures::all()).validate_all(&wasm)?; + Ok(()) + } + #[test] fn package_scope_type_only_package_decode_fails() -> Result<()> { let mut resolve = Resolve::new(); @@ -364,4 +411,247 @@ record point { } Ok(()) } + + /// An `interface` or `world` names a foreign package-scope type with an + /// `import` on its wrapping component-type. A package-scope definition has + /// no such wrapper, so its import goes on the package's component itself. + #[test] + fn package_scope_type_depending_on_foreign_package_type() -> Result<()> { + let mut resolve = Resolve::new(); + resolve.push_str( + "types.wit", + r#" +package local:types; + +record point { + x: u32, + y: u32, +} + +interface unused {} +"#, + )?; + let pkg = resolve.push_str( + "consumer.wit", + r#" +package local:consumer; + +use local:types/point; + +record bin { + p: point, +} + +interface api { + wrap: func(b: bin); +} +"#, + )?; + + let wasm = encode(&resolve, pkg)?; + let wat = wasmprinter::print_bytes(&wasm)?; + assert!( + wat.contains("(import \"local:types/point\" (type"), + "expected a qualified import on the package itself:\n{wat}" + ); + wasmparser::Validator::new_with_features(WasmFeatures::all()).validate_all(&wasm)?; + + // The import must not be mistaken for a concrete component's, and + // `point` must stay owned by `local:types` rather than being claimed by + // the consumer. + let decoded = crate::decode(&wasm)?; + let mut printer = crate::WitPrinter::default(); + printer.print(decoded.resolve(), decoded.package(), &[])?; + let printed = printer.output.to_string(); + assert!(printed.contains("use local:types/point;"), "{printed}"); + assert!(printed.contains("record bin {"), "{printed}"); + assert!( + !printed.contains("record point"), + "the foreign definition must not be restated as our own:\n{printed}" + ); + + Ok(()) + } + + /// A foreign package-scope type carries its version in the fully-qualified + /// import name and that version survives a decode round-trip. + #[test] + fn package_scope_foreign_type_preserves_version() -> Result<()> { + let mut resolve = Resolve::new(); + resolve.push_str( + "types.wit", + r#" +package local:types@1.2.3; + +record point { + x: u32, + y: u32, +} + +interface unused {} +"#, + )?; + let pkg = resolve.push_str( + "consumer.wit", + r#" +package local:consumer@0.1.0; + +use local:types/point@1.2.3; + +interface api { + move-to: func(p: point); +} + +world w { + export api; +} +"#, + )?; + + let wasm = encode(&resolve, pkg)?; + let wat = wasmprinter::print_bytes(&wasm)?; + assert!( + wat.contains("local:types/point@1.2.3"), + "expected the import name to carry the version:\n{wat}" + ); + wasmparser::Validator::new_with_features(WasmFeatures::all()).validate_all(&wasm)?; + + let decoded = crate::decode(&wasm)?; + let mut printer = crate::WitPrinter::default(); + printer.print(decoded.resolve(), decoded.package(), &[])?; + let printed = printer.output.to_string(); + assert!( + printed.contains("use local:types/point@1.2.3;"), + "expected the decoded use to keep the version:\n{printed}" + ); + + Ok(()) + } + + /// `resource` is excluded from package scope: its bound is abstract, so it + /// can't be restated and bound with `eq`. A component that nonetheless names + /// a resource where a package-scope type would go must be rejected rather + /// than silently decoded. + #[test] + fn package_scope_resource_import_is_rejected() -> Result<()> { + let wasm = wat::parse_str( + r#" +(component + (import "local:types/thing" (type (;0;) (sub resource))) + (type (;1;) (record (field "x" u32))) + (export (;2;) "bin" (type 1)) +) +"#, + )?; + + let err = match crate::decode(&wasm) { + Ok(_) => panic!("resource at package scope must fail to decode"), + Err(err) => format!("{err:#}"), + }; + assert!( + err.contains("not a structural type"), + "unexpected error: {err}" + ); + Ok(()) + } +} + +#[cfg(all(test, feature = "dummy-module"))] +mod component_tests { + use crate::{ + ComponentEncoder, DecodedWasm, StringEncoding, dummy_module, embed_component_metadata, + }; + use anyhow::Result; + use wit_parser::{ManglingAndAbi, Resolve}; + + // Note: `wit_parser::{Type, TypeOwner, WorldItem}` are referenced via full + // paths in the assertions below to keep this module's imports minimal. + + /// Building a real component from a world that uses a package-scope type + /// must not fabricate an interface just to hold the type: the export is the + /// interface itself. This is the outcome motivating the feature (issue + /// #694). The produced artifact must also decode as a component, not be + /// mistaken for a WIT package now that packages may carry type imports. + #[test] + fn package_scope_component_has_no_invented_interface() -> Result<()> { + let mut resolve = Resolve::new(); + let pkg = resolve.push_str( + "demo.wit", + r#" +package local:demo; + +record point { + x: u32, + y: u32, +} + +interface api { + move-to: func(p: point); +} + +world w { + export api; +} +"#, + )?; + let world = resolve.select_world(&[pkg], Some("w"))?; + + let mut module = dummy_module(&resolve, world, ManglingAndAbi::Standard32); + embed_component_metadata(&mut module, &resolve, world, StringEncoding::UTF8)?; + let component = ComponentEncoder::default() + .module(&module)? + .validate(true) + .encode()?; + + let decoded = crate::decode(&component)?; + let (resolve, world) = match &decoded { + DecodedWasm::Component(resolve, world) => (resolve, *world), + DecodedWasm::WitPackage(..) => { + panic!("a real component must not be sniffed as a WIT package") + } + }; + + // The world exports the interface directly, with no import fabricated + // to carry the package-scope type. + assert!( + resolve.worlds[world].imports.is_empty(), + "no import should be invented to hold the package-scope type" + ); + let exports: Vec<_> = resolve.worlds[world].exports.values().collect(); + assert_eq!( + exports.len(), + 1, + "world should export exactly the interface" + ); + let iface = match exports[0] { + wit_parser::WorldItem::Interface { id, .. } => *id, + other => panic!("expected an interface export, got {other:?}"), + }; + + // That interface still carries `move-to`, and its parameter is the + // package-scope `point`, owned by a package rather than by the + // interface. + let func = resolve.interfaces[iface] + .functions + .get("move-to") + .expect("api interface should keep its function"); + let point = match func.params.as_slice() { + [param] => match param.ty { + wit_parser::Type::Id(id) => id, + other => panic!("expected a named type param, got {other:?}"), + }, + params => panic!("expected one param, got {}", params.len()), + }; + // Decoding a component doesn't reconstruct package scope, but the type + // must still resolve to the sole `api` interface rather than to any + // separate interface fabricated to hold it. + assert_eq!( + resolve.types[point].owner, + wit_parser::TypeOwner::Interface(iface), + "point should belong to the api interface, not an invented one" + ); + assert_eq!(resolve.types[point].name.as_deref(), Some("point")); + + Ok(()) + } } diff --git a/crates/wit-component/src/printing.rs b/crates/wit-component/src/printing.rs index 04e1cce1d0..9b49e15e55 100644 --- a/crates/wit-component/src/printing.rs +++ b/crates/wit-component/src/printing.rs @@ -78,6 +78,8 @@ impl WitPrinter { self.output.indent_start(); } + self.print_foreign_package_type_uses(resolve, pkg)?; + for (name, id) in package.types.iter() { let ty = &resolve.types[*id]; self.print_docs(&ty.docs); @@ -90,8 +92,6 @@ impl WitPrinter { } } - self.print_foreign_package_type_uses(resolve, pkg)?; - for (name, id) in package.interfaces.iter() { self.print_interface_outer(resolve, *id, name)?; self.output.indent_start(); @@ -119,9 +119,12 @@ impl WitPrinter { } /// Emit toplevel `use ns:pkg/name;` for foreign package-scope types that - /// are referenced from this package's interfaces/worlds. + /// this package's types, interfaces, or worlds refer to. fn print_foreign_package_type_uses(&mut self, resolve: &Resolve, pkg: PackageId) -> Result<()> { let mut live = LiveTypes::default(); + for (_, &id) in resolve.packages[pkg].types.iter() { + live.add_type_id(resolve, id); + } for (_, &id) in resolve.packages[pkg].interfaces.iter() { live.add_interface(resolve, id); } diff --git a/crates/wit-component/tests/interfaces/package-scope-docs.wat b/crates/wit-component/tests/interfaces/package-scope-docs.wat index 07af1d25d5..3aa061775d 100644 --- a/crates/wit-component/tests/interfaces/package-scope-docs.wat +++ b/crates/wit-component/tests/interfaces/package-scope-docs.wat @@ -3,15 +3,16 @@ (export (;1;) "point" (type 0)) (type (;2;) (component - (type (;0;) + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (type (;1;) (eq 0))) + (type (;2;) (instance - (type (;0;) (record (field "x" u32) (field "y" u32))) - (export (;1;) "point" (type (eq 0))) - (type (;2;) (func (param "p" 1))) - (export (;0;) "move-to" (func (type 2))) + (alias outer 1 1 (type (;0;))) + (type (;1;) (func (param "p" 0))) + (export (;0;) "move-to" (func (type 1))) ) ) - (export (;0;) "local:demo/api" (instance (type 0))) + (export (;0;) "local:demo/api" (instance (type 2))) ) ) (export (;3;) "api" (type 2)) @@ -19,15 +20,16 @@ (component (type (;0;) (component - (type (;0;) + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (type (;1;) (eq 0))) + (type (;2;) (instance - (type (;0;) (record (field "x" u32) (field "y" u32))) - (export (;1;) "point" (type (eq 0))) - (type (;2;) (func (param "p" 1))) - (export (;0;) "move-to" (func (type 2))) + (alias outer 1 1 (type (;0;))) + (type (;1;) (func (param "p" 0))) + (export (;0;) "move-to" (func (type 1))) ) ) - (export (;0;) "local:demo/api" (instance (type 0))) + (export (;0;) "local:demo/api" (instance (type 2))) ) ) (export (;0;) "local:demo/w" (component (type 0))) diff --git a/crates/wit-component/tests/interfaces/package-scope-field-docs.wat b/crates/wit-component/tests/interfaces/package-scope-field-docs.wat index 8a941ce350..a8fd041f4a 100644 --- a/crates/wit-component/tests/interfaces/package-scope-field-docs.wat +++ b/crates/wit-component/tests/interfaces/package-scope-field-docs.wat @@ -9,23 +9,26 @@ (export (;7;) "direction" (type 6)) (type (;8;) (component - (type (;0;) + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (type (;1;) (eq 0))) + (type (;2;) (variant (case "circle") (case "rect" 1))) + (import "local:demo/shape" (type (;3;) (eq 2))) + (type (;4;) (flags "bold" "italic")) + (import "local:demo/style" (type (;5;) (eq 4))) + (type (;6;) (enum "north" "south")) + (import "local:demo/direction" (type (;7;) (eq 6))) + (type (;8;) (instance - (type (;0;) (record (field "x" u32) (field "y" u32))) - (export (;1;) "point" (type (eq 0))) - (type (;2;) (variant (case "circle") (case "rect" 1))) - (export (;3;) "shape" (type (eq 2))) - (type (;4;) (flags "bold" "italic")) - (export (;5;) "style" (type (eq 4))) - (type (;6;) (func (param "s" 3) (param "style" 5))) - (export (;0;) "draw" (func (type 6))) - (type (;7;) (enum "north" "south")) - (export (;8;) "direction" (type (eq 7))) - (type (;9;) (func (result 8))) - (export (;1;) "heading" (func (type 9))) + (alias outer 1 3 (type (;0;))) + (alias outer 1 5 (type (;1;))) + (type (;2;) (func (param "s" 0) (param "style" 1))) + (export (;0;) "draw" (func (type 2))) + (alias outer 1 7 (type (;3;))) + (type (;4;) (func (result 3))) + (export (;1;) "heading" (func (type 4))) ) ) - (export (;0;) "local:demo/api" (instance (type 0))) + (export (;0;) "local:demo/api" (instance (type 8))) ) ) (export (;9;) "api" (type 8)) @@ -33,23 +36,26 @@ (component (type (;0;) (component - (type (;0;) + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (type (;1;) (eq 0))) + (type (;2;) (variant (case "circle") (case "rect" 1))) + (import "local:demo/shape" (type (;3;) (eq 2))) + (type (;4;) (flags "bold" "italic")) + (import "local:demo/style" (type (;5;) (eq 4))) + (type (;6;) (enum "north" "south")) + (import "local:demo/direction" (type (;7;) (eq 6))) + (type (;8;) (instance - (type (;0;) (record (field "x" u32) (field "y" u32))) - (export (;1;) "point" (type (eq 0))) - (type (;2;) (variant (case "circle") (case "rect" 1))) - (export (;3;) "shape" (type (eq 2))) - (type (;4;) (flags "bold" "italic")) - (export (;5;) "style" (type (eq 4))) - (type (;6;) (func (param "s" 3) (param "style" 5))) - (export (;0;) "draw" (func (type 6))) - (type (;7;) (enum "north" "south")) - (export (;8;) "direction" (type (eq 7))) - (type (;9;) (func (result 8))) - (export (;1;) "heading" (func (type 9))) + (alias outer 1 3 (type (;0;))) + (alias outer 1 5 (type (;1;))) + (type (;2;) (func (param "s" 0) (param "style" 1))) + (export (;0;) "draw" (func (type 2))) + (alias outer 1 7 (type (;3;))) + (type (;4;) (func (result 3))) + (export (;1;) "heading" (func (type 4))) ) ) - (export (;0;) "local:demo/api" (instance (type 0))) + (export (;0;) "local:demo/api" (instance (type 8))) ) ) (export (;0;) "local:demo/w" (component (type 0))) diff --git a/crates/wit-component/tests/interfaces/package-scope-foreign-use.wat b/crates/wit-component/tests/interfaces/package-scope-foreign-use.wat new file mode 100644 index 0000000000..1d3ea3320d --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-foreign-use.wat @@ -0,0 +1,55 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "local:types/point" (type (;1;) (eq 0))) + (type (;2;) (record (field "p" 1))) + (export (;3;) "bin" (type 2)) + (type (;4;) + (component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "local:types/point" (type (;1;) (eq 0))) + (type (;2;) (record (field "p" 1))) + (import "local:consumer/bin" (type (;3;) (eq 2))) + (type (;4;) + (instance + (alias outer 1 3 (type (;0;))) + (type (;1;) (func (param "b" 0))) + (export (;0;) "wrap" (func (type 1))) + (alias outer 1 1 (type (;2;))) + (type (;3;) (func (param "p" 2))) + (export (;1;) "move-to" (func (type 3))) + ) + ) + (export (;0;) "local:consumer/api" (instance (type 4))) + ) + ) + (export (;5;) "api" (type 4)) + (type (;6;) + (component + (type (;0;) + (component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "local:types/point" (type (;1;) (eq 0))) + (type (;2;) (record (field "p" 1))) + (import "local:consumer/bin" (type (;3;) (eq 2))) + (type (;4;) + (instance + (alias outer 1 3 (type (;0;))) + (type (;1;) (func (param "b" 0))) + (export (;0;) "wrap" (func (type 1))) + (alias outer 1 1 (type (;2;))) + (type (;3;) (func (param "p" 2))) + (export (;1;) "move-to" (func (type 3))) + ) + ) + (export (;0;) "local:consumer/api" (instance (type 4))) + ) + ) + (export (;0;) "local:consumer/w" (component (type 0))) + ) + ) + (export (;7;) "w" (type 6)) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit b/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit new file mode 100644 index 0000000000..a1703d92f3 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit @@ -0,0 +1,16 @@ +package local:consumer; + +use local:types/point; + +record bin { + p: point, +} + +interface api { + wrap: func(b: bin); + move-to: func(p: point); +} + +world w { + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit.print b/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit.print new file mode 100644 index 0000000000..b267a94634 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-foreign-use/consumer.wit.print @@ -0,0 +1,17 @@ +package local:consumer; + +use local:types/point; + +record bin { + p: point, +} + +interface api { + wrap: func(b: bin); + + move-to: func(p: point); +} + +world w { + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-foreign-use/deps/types/types.wit b/crates/wit-component/tests/interfaces/package-scope-foreign-use/deps/types/types.wit new file mode 100644 index 0000000000..d25c1fd54a --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-foreign-use/deps/types/types.wit @@ -0,0 +1,10 @@ +package local:types; + +/// Shared package-scope point. +record point { + x: u32, + y: u32, +} + +interface unused { +} diff --git a/crates/wit-component/tests/interfaces/package-scope-nested.wat b/crates/wit-component/tests/interfaces/package-scope-nested.wat index f7caec2f00..9da09ac215 100644 --- a/crates/wit-component/tests/interfaces/package-scope-nested.wat +++ b/crates/wit-component/tests/interfaces/package-scope-nested.wat @@ -3,15 +3,16 @@ (export (;1;) "point" (type 0)) (type (;2;) (component - (type (;0;) + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "local:nested/point" (type (;1;) (eq 0))) + (type (;2;) (instance - (type (;0;) (record (field "x" u32) (field "y" u32))) - (export (;1;) "point" (type (eq 0))) - (type (;2;) (func (param "p" 1))) - (export (;0;) "move-to" (func (type 2))) + (alias outer 1 1 (type (;0;))) + (type (;1;) (func (param "p" 0))) + (export (;0;) "move-to" (func (type 1))) ) ) - (export (;0;) "local:nested/api" (instance (type 0))) + (export (;0;) "local:nested/api" (instance (type 2))) ) ) (export (;3;) "api" (type 2)) @@ -19,15 +20,16 @@ (component (type (;0;) (component - (type (;0;) + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "local:nested/point" (type (;1;) (eq 0))) + (type (;2;) (instance - (type (;0;) (record (field "x" u32) (field "y" u32))) - (export (;1;) "point" (type (eq 0))) - (type (;2;) (func (param "p" 1))) - (export (;0;) "move-to" (func (type 2))) + (alias outer 1 1 (type (;0;))) + (type (;1;) (func (param "p" 0))) + (export (;0;) "move-to" (func (type 1))) ) ) - (export (;0;) "local:nested/api" (instance (type 0))) + (export (;0;) "local:nested/api" (instance (type 2))) ) ) (export (;0;) "local:nested/w" (component (type 0))) diff --git a/crates/wit-component/tests/interfaces/package-scope-spec-interface.wat b/crates/wit-component/tests/interfaces/package-scope-spec-interface.wat new file mode 100644 index 0000000000..ee088f5cf2 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-spec-interface.wat @@ -0,0 +1,23 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type 0)) + (type (;2;) + (component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (type (;1;) (eq 0))) + (type (;2;) + (instance + (alias outer 1 1 (type (;0;))) + (type (;1;) (func (param "p" 0))) + (export (;0;) "move-to" (func (type 1))) + ) + ) + (export (;0;) "local:demo/api" (instance (type 2))) + ) + ) + (export (;3;) "api" (type 2)) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-spec-interface.wit b/crates/wit-component/tests/interfaces/package-scope-spec-interface.wit new file mode 100644 index 0000000000..65d3074983 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-spec-interface.wit @@ -0,0 +1,7 @@ +package local:demo; + +record point { x: u32, y: u32 } + +interface api { + move-to: func(p: point); +} diff --git a/crates/wit-component/tests/interfaces/package-scope-spec-interface.wit.print b/crates/wit-component/tests/interfaces/package-scope-spec-interface.wit.print new file mode 100644 index 0000000000..365af4dfc2 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-spec-interface.wit.print @@ -0,0 +1,11 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +interface api { + move-to: func(p: point); +} + diff --git a/crates/wit-component/tests/interfaces/package-scope-spec-world.wat b/crates/wit-component/tests/interfaces/package-scope-spec-world.wat new file mode 100644 index 0000000000..35904c346d --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-spec-world.wat @@ -0,0 +1,22 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type 0)) + (type (;2;) + (component + (type (;0;) + (component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (type (;1;) (eq 0))) + (type (;2;) (func (param "p" 1))) + (export (;0;) "move-to" (func (type 2))) + ) + ) + (export (;0;) "local:demo/the-world" (component (type 0))) + ) + ) + (export (;3;) "the-world" (type 2)) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-spec-world.wit b/crates/wit-component/tests/interfaces/package-scope-spec-world.wit new file mode 100644 index 0000000000..db7f6be522 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-spec-world.wit @@ -0,0 +1,7 @@ +package local:demo; + +record point { x: u32, y: u32 } + +world the-world { + export move-to: func(p: point); +} diff --git a/crates/wit-component/tests/interfaces/package-scope-spec-world.wit.print b/crates/wit-component/tests/interfaces/package-scope-spec-world.wit.print new file mode 100644 index 0000000000..3c61d751e9 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-spec-world.wit.print @@ -0,0 +1,10 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +world the-world { + export move-to: func(p: point); +} diff --git a/crates/wit-component/tests/interfaces/package-scope-types.wat b/crates/wit-component/tests/interfaces/package-scope-types.wat index 417e8aa2e6..a0d3af5e11 100644 --- a/crates/wit-component/tests/interfaces/package-scope-types.wat +++ b/crates/wit-component/tests/interfaces/package-scope-types.wat @@ -11,29 +11,34 @@ (export (;9;) "path" (type 8)) (type (;10;) (component - (type (;0;) + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (type (;1;) (eq 0))) + (type (;2;) (enum "north" "south" "east" "west")) + (import "local:demo/direction" (type (;3;) (eq 2))) + (type (;4;) (list 1)) + (import "local:demo/path" (type (;5;) (eq 4))) + (type (;6;) (variant (case "circle" u32) (case "rect" 1))) + (import "local:demo/shape" (type (;7;) (eq 6))) + (type (;8;) (flags "bold" "italic")) + (import "local:demo/style" (type (;9;) (eq 8))) + (type (;10;) (instance - (type (;0;) (record (field "x" u32) (field "y" u32))) - (export (;1;) "point" (type (eq 0))) - (type (;2;) (func (param "p" 1))) - (export (;0;) "move-to" (func (type 2))) - (type (;3;) (enum "north" "south" "east" "west")) - (export (;4;) "direction" (type (eq 3))) + (alias outer 1 1 (type (;0;))) + (type (;1;) (func (param "p" 0))) + (export (;0;) "move-to" (func (type 1))) + (alias outer 1 3 (type (;2;))) + (type (;3;) (func (result 2))) + (export (;1;) "heading" (func (type 3))) + (alias outer 1 5 (type (;4;))) (type (;5;) (func (result 4))) - (export (;1;) "heading" (func (type 5))) - (type (;6;) (list 1)) - (export (;7;) "path" (type (eq 6))) - (type (;8;) (func (result 7))) - (export (;2;) "trail" (func (type 8))) - (type (;9;) (variant (case "circle" u32) (case "rect" 1))) - (export (;10;) "shape" (type (eq 9))) - (type (;11;) (flags "bold" "italic")) - (export (;12;) "style" (type (eq 11))) - (type (;13;) (func (param "s" 10) (param "style" 12))) - (export (;3;) "draw" (func (type 13))) + (export (;2;) "trail" (func (type 5))) + (alias outer 1 7 (type (;6;))) + (alias outer 1 9 (type (;7;))) + (type (;8;) (func (param "s" 6) (param "style" 7))) + (export (;3;) "draw" (func (type 8))) ) ) - (export (;0;) "local:demo/api" (instance (type 0))) + (export (;0;) "local:demo/api" (instance (type 10))) ) ) (export (;11;) "api" (type 10)) @@ -41,38 +46,42 @@ (component (type (;0;) (component - (type (;0;) + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (type (;1;) (eq 0))) + (type (;2;) (instance - (type (;0;) (record (field "x" u32) (field "y" u32))) - (export (;1;) "point" (type (eq 0))) - (type (;2;) (func (param "p" 1))) - (export (;0;) "place" (func (type 2))) + (alias outer 1 1 (type (;0;))) + (type (;1;) (func (param "p" 0))) + (export (;0;) "place" (func (type 1))) ) ) - (import "host" (instance (;0;) (type 0))) - (type (;1;) + (import "host" (instance (;0;) (type 2))) + (type (;3;) (enum "north" "south" "east" "west")) + (import "local:demo/direction" (type (;4;) (eq 3))) + (type (;5;) (list 1)) + (import "local:demo/path" (type (;6;) (eq 5))) + (type (;7;) (variant (case "circle" u32) (case "rect" 1))) + (import "local:demo/shape" (type (;8;) (eq 7))) + (type (;9;) (flags "bold" "italic")) + (import "local:demo/style" (type (;10;) (eq 9))) + (type (;11;) (instance - (type (;0;) (record (field "x" u32) (field "y" u32))) - (export (;1;) "point" (type (eq 0))) - (type (;2;) (func (param "p" 1))) - (export (;0;) "move-to" (func (type 2))) - (type (;3;) (enum "north" "south" "east" "west")) - (export (;4;) "direction" (type (eq 3))) + (alias outer 1 1 (type (;0;))) + (type (;1;) (func (param "p" 0))) + (export (;0;) "move-to" (func (type 1))) + (alias outer 1 4 (type (;2;))) + (type (;3;) (func (result 2))) + (export (;1;) "heading" (func (type 3))) + (alias outer 1 6 (type (;4;))) (type (;5;) (func (result 4))) - (export (;1;) "heading" (func (type 5))) - (type (;6;) (list 1)) - (export (;7;) "path" (type (eq 6))) - (type (;8;) (func (result 7))) - (export (;2;) "trail" (func (type 8))) - (type (;9;) (variant (case "circle" u32) (case "rect" 1))) - (export (;10;) "shape" (type (eq 9))) - (type (;11;) (flags "bold" "italic")) - (export (;12;) "style" (type (eq 11))) - (type (;13;) (func (param "s" 10) (param "style" 12))) - (export (;3;) "draw" (func (type 13))) + (export (;2;) "trail" (func (type 5))) + (alias outer 1 8 (type (;6;))) + (alias outer 1 10 (type (;7;))) + (type (;8;) (func (param "s" 6) (param "style" 7))) + (export (;3;) "draw" (func (type 8))) ) ) - (export (;1;) "local:demo/api" (instance (type 1))) + (export (;1;) "local:demo/api" (instance (type 11))) ) ) (export (;0;) "local:demo/w" (component (type 0))) diff --git a/crates/wit-component/tests/interfaces/package-scope-world-only.wat b/crates/wit-component/tests/interfaces/package-scope-world-only.wat index f1ae208e15..1e36cdb79e 100644 --- a/crates/wit-component/tests/interfaces/package-scope-world-only.wat +++ b/crates/wit-component/tests/interfaces/package-scope-world-only.wat @@ -6,7 +6,7 @@ (type (;0;) (component (type (;0;) (record (field "x" u32) (field "y" u32))) - (import "point" (type (;1;) (eq 0))) + (import "local:demo/point" (type (;1;) (eq 0))) (type (;2;) (func (param "p" 1))) (import "move-to" (func (;0;) (type 2))) (export (;1;) "place" (func (type 2))) diff --git a/crates/wit-parser/src/decoding.rs b/crates/wit-parser/src/decoding.rs index 1b4865b06c..ed72c709c4 100644 --- a/crates/wit-parser/src/decoding.rs +++ b/crates/wit-parser/src/decoding.rs @@ -151,17 +151,21 @@ impl ComponentInfo { return None; } - if !self.externs.iter().all(|(_, item)| { - let export = match item { - Extern::Export(e) => e, - _ => return false, - }; - match export.ty { + if !self.externs.iter().all(|(name, item)| match item { + Extern::Export(export) => match export.ty { ComponentEntityType::Type { created, .. } => matches!( created, ComponentAnyTypeId::Component(_) | ComponentAnyTypeId::Defined(_) ), _ => false, + }, + + // A package's definitions are all exported, with one exception: a + // package-scope type defined in terms of one from another package + // has no wrapping component-type to hold the `import` naming where + // that definition came from, so the import lands here instead. + Extern::Import(import) => { + matches!(import.ty, ComponentEntityType::Type { .. }) && is_qualified_name(name) } }) { return None; @@ -171,7 +175,11 @@ impl ComponentInfo { // strings for each component. The v1 format uses ":/wit" as the name // for the top-level exports, while the v2 format uses the unqualified name of the encoded // entity. - match ComponentName::new(&self.externs[0].0, 0).ok()?.kind() { + let (first_export, _) = self + .externs + .iter() + .find(|(_, item)| matches!(item, Extern::Export(_)))?; + match ComponentName::new(first_export, 0).ok()?.kind() { ComponentNameKind::Interface(name) if name.interface().as_str() == "wit" => { Some(WitEncodingVersion::V1) } @@ -225,7 +233,17 @@ impl ComponentInfo { for (export_name, item) in self.externs.iter() { let export = match item { Extern::Export(e) => e, - _ => unreachable!(), + + // An import here names a package-scope type from another + // package that this package's own definitions depend on. It's + // necessarily foreign, so there's no local definition to + // reconcile it with yet. + Extern::Import(import) => { + decoder + .register_package_type_import(export_name, import, None, &mut fields) + .with_context(|| format!("failed to process import `{export_name}`"))?; + continue; + } }; match export.ty { ComponentEntityType::Type { @@ -577,13 +595,6 @@ impl WitPackageDecoder<'_> { _ => unreachable!(), }; - // Process all imports for this package first, where imports are - // importing from remote packages. - for (name, item) in ty.imports.iter() { - self.register_import(name, item) - .with_context(|| format!("failed to process import `{name}`"))?; - } - let mut package = Package { // The name encoded for packages must be of the form `foo:bar/wit` // where "wit" is just a placeholder for now. The package name in @@ -600,12 +611,20 @@ impl WitPackageDecoder<'_> { types: Default::default(), }; + let package_name = package.name.clone(); let mut fields = PackageFields { interfaces: &mut package.interfaces, worlds: &mut package.worlds, types: &mut package.types, }; + // Process all imports for this package first, where imports are + // importing from remote packages. + for (name, item) in ty.imports.iter() { + self.register_component_type_import(name, item, &package_name, &mut fields) + .with_context(|| format!("failed to process import `{name}`"))?; + } + for (name, ty) in ty.exports.iter() { match ty.ty { ComponentEntityType::Instance(_) => { @@ -614,7 +633,7 @@ impl WitPackageDecoder<'_> { } ComponentEntityType::Component(idx) => { let ty = &self.types[idx]; - self.register_world(name.as_str(), ty, &mut fields) + self.register_world(name.as_str(), ty, &package_name, &mut fields) .with_context(|| format!("failed to process export `{name}`"))?; } ComponentEntityType::Type { @@ -649,9 +668,9 @@ impl WitPackageDecoder<'_> { _ => bail!("expected world name to be fully qualified"), }; - for (name, ty) in imports.iter() { - self.register_import(name, ty) - .with_context(|| format!("failed to process import `{name}`"))?; + for (import_name, ty) in imports.iter() { + self.register_component_type_import(import_name, ty, &package, fields) + .with_context(|| format!("failed to process import `{import_name}`"))?; } let _ = self.register_interface(name, item, fields)?; @@ -659,6 +678,112 @@ impl WitPackageDecoder<'_> { Ok(package) } + /// Processes one import of a wrapping component-type, which is either an + /// instance for a `use`d interface or a type for a package-scope type. + fn register_component_type_import( + &mut self, + name: &str, + item: &ComponentItem, + package: &PackageName, + fields: &mut PackageFields<'_>, + ) -> Result<()> { + match item.ty { + ComponentEntityType::Type { .. } => { + self.register_package_type_import(name, item, Some(package), fields)?; + } + _ => { + self.register_import(name, item)?; + } + } + Ok(()) + } + + /// Registers a dependency on the package-scope type named by `name`, a + /// fully-qualified `namespace:package/name`. + /// + /// Package-scope types are structural, so the importer restates the + /// definition and binds it with an `eq` import rather than supplying an + /// instance to satisfy it. The restated definition is unified with the type + /// already decoded for `package`, when the name belongs to it, or with a + /// lazily created type otherwise. `package` is `None` where no local + /// definition can be in scope yet, which makes the name necessarily foreign. + fn register_package_type_import( + &mut self, + name: &str, + item: &ComponentItem, + package: Option<&PackageName>, + fields: &mut PackageFields<'_>, + ) -> Result { + let parsed = self.parse_component_name(name)?; + let owner_name = match parsed.kind() { + ComponentNameKind::Interface(id) => id, + _ => bail!("expected type import `{name}` to be fully qualified"), + }; + let owner = owner_name.to_package_name(item)?; + let type_name = owner_name.interface().to_string(); + + let (referenced, created) = match item.ty { + ComponentEntityType::Type { + referenced, + created, + } => (referenced, created), + _ => unreachable!(), + }; + let referenced = match referenced { + ComponentAnyTypeId::Defined(ty) => ty, + // `resource` is excluded from package scope precisely because its + // bound is abstract and so can't be restated like this. + _ => bail!("package-scope type `{name}` is not a structural type"), + }; + + let existing = if package == Some(&owner) { + fields.types.get(&type_name).copied() + } else { + None + } + .or_else(|| { + self.foreign_packages + .get(&owner.to_string()) + .and_then(|pkg| pkg.types.get(&type_name).copied()) + }); + + let id = match existing { + Some(id) => { + // Walk the restated definition to unify its anonymous inner + // types with those of the original definition, the same way the + // types of a `use`d interface are unified. + let types = self.types; + self.register_defined(id, &types[referenced])?; + let prev = self.type_map.insert(created, id); + assert!(prev.is_none()); + id + } + + // Nothing has declared this definition yet, so fill it in here. + // This is the only view available when a definition is decoded on + // its own, such as a `world` out of a `component-type` custom + // section, and it leaves the owning package with just the slice of + // types this resolve needs. + None => { + let id = self.register_type_export(&type_name, item, TypeOwner::None)?; + let package = self + .foreign_packages + .entry(owner.to_string()) + .or_insert_with(|| Package { + name: owner.clone(), + docs: Default::default(), + interfaces: Default::default(), + worlds: Default::default(), + types: Default::default(), + }); + package.types.insert(type_name, id); + id + } + }; + + Ok(id) + } + fn decode_world<'a>( &mut self, name: &str, @@ -678,7 +803,7 @@ impl WitPackageDecoder<'_> { _ => bail!("expected world name to be fully qualified"), }; - let _ = self.register_world(name, ty, fields)?; + let _ = self.register_world(name, ty, &package, fields)?; Ok(package) } @@ -1152,6 +1277,7 @@ impl WitPackageDecoder<'_> { &mut self, name: &str, ty: &ComponentType, + package_name: &PackageName, package: &mut PackageFields<'a>, ) -> Result { let name = self @@ -1174,6 +1300,13 @@ impl WitPackageDecoder<'_> { ComponentEntityType::Instance(_) => { self.decode_world_instance(name, item, package)? } + // A fully-qualified name here is a package-scope type the world + // depends on, which is not an item of the world itself. A plain + // name is a type the world defines. + ComponentEntityType::Type { .. } if is_qualified_name(name) => { + self.register_package_type_import(name, item, Some(package_name), package)?; + continue; + } ComponentEntityType::Type { .. } => { let ty = self.register_type_export(name.as_str(), item, owner)?; ( @@ -1613,119 +1746,9 @@ impl WitPackageDecoder<'_> { self.resolve.types[id].owner = TypeOwner::Package(pkg); } - // Encoding inlines package-scope types into interface/world instances - // and exports them for validation. Drop those duplicates so round-trip - // WIT keeps package ownership (bare names, no invented interface decls). - self.coalesce_inlined_package_types(pkg); - pkg } - /// Rewrite interface/world references that duplicate package-scope types. - fn coalesce_inlined_package_types(&mut self, pkg: PackageId) { - let package_types = self.resolve.packages[pkg].types.clone(); - if package_types.is_empty() { - return; - } - - let mut remap = HashMap::new(); - let iface_ids: Vec<_> = self.resolve.packages[pkg] - .interfaces - .values() - .copied() - .collect(); - for iface_id in iface_ids { - self.coalesce_package_types_in_interface(iface_id, &package_types, &mut remap); - } - - let world_ids: Vec<_> = self.resolve.packages[pkg] - .worlds - .values() - .copied() - .collect(); - for world_id in world_ids { - let mut remove = Vec::new(); - let mut inline_ifaces = Vec::new(); - for (key, item) in self.resolve.worlds[world_id].imports.iter() { - match (key, item) { - (WorldKey::Name(name), WorldItem::Type { id, .. }) => { - if let Some(&pkg_ty) = package_types.get(name) { - if *id != pkg_ty { - remap.insert(*id, pkg_ty); - remove.push(key.clone()); - } - } - } - (_, WorldItem::Interface { id, .. }) => { - inline_ifaces.push(*id); - } - _ => {} - } - } - for (_key, item) in self.resolve.worlds[world_id].exports.iter() { - if let WorldItem::Interface { id, .. } = item { - inline_ifaces.push(*id); - } - } - for key in remove { - self.resolve.worlds[world_id].imports.swap_remove(&key); - } - for iface_id in inline_ifaces { - // Named package interfaces are handled above; still coalesce - // anonymous ifaces reachable from worlds. - if self.resolve.interfaces[iface_id].name.is_none() { - self.coalesce_package_types_in_interface(iface_id, &package_types, &mut remap); - } - } - } - - if remap.is_empty() { - return; - } - - for (_id, ty) in self.resolve.types.iter_mut() { - rewrite_type_ids_in_kind(&mut ty.kind, &remap); - } - for (_id, iface) in self.resolve.interfaces.iter_mut() { - for func in iface.functions.values_mut() { - rewrite_type_ids_in_func(func, &remap); - } - } - for (_id, world) in self.resolve.worlds.iter_mut() { - for item in world.imports.values_mut().chain(world.exports.values_mut()) { - match item { - WorldItem::Function(f) => rewrite_type_ids_in_func(f, &remap), - WorldItem::Type { id, .. } => { - if let Some(&new) = remap.get(id) { - *id = new; - } - } - WorldItem::Interface { .. } => {} - } - } - } - } - - fn coalesce_package_types_in_interface( - &mut self, - iface_id: InterfaceId, - package_types: &IndexMap, - remap: &mut HashMap, - ) { - let mut remove = Vec::new(); - for (name, &ty_id) in self.resolve.interfaces[iface_id].types.iter() { - if let Some(&pkg_ty) = package_types.get(name) { - if ty_id != pkg_ty { - remap.insert(ty_id, pkg_ty); - remove.push(name.clone()); - } - } - } - for name in remove { - self.resolve.interfaces[iface_id].types.swap_remove(&name); - } - } - fn visit_package(&self, idx: usize, order: &mut IndexSet) { if order.contains(&idx) { return; @@ -2046,76 +2069,11 @@ impl InterfaceNameExt for wasmparser::names::InterfaceName<'_> { } } -fn rewrite_type_id(id: &mut TypeId, remap: &HashMap) { - if let Some(new) = remap.get(id) { - *id = *new; - } -} - -fn rewrite_type(ty: &mut Type, remap: &HashMap) { - if let Type::Id(id) = ty { - rewrite_type_id(id, remap); - } -} - -fn rewrite_type_ids_in_kind(kind: &mut TypeDefKind, remap: &HashMap) { - match kind { - TypeDefKind::Record(r) => { - for field in r.fields.iter_mut() { - rewrite_type(&mut field.ty, remap); - } - } - TypeDefKind::Tuple(t) => { - for ty in t.types.iter_mut() { - rewrite_type(ty, remap); - } - } - TypeDefKind::Variant(v) => { - for case in v.cases.iter_mut() { - if let Some(ty) = &mut case.ty { - rewrite_type(ty, remap); - } - } - } - TypeDefKind::Option(t) | TypeDefKind::List(t) | TypeDefKind::Type(t) => { - rewrite_type(t, remap); - } - TypeDefKind::FixedLengthList(t, _) => rewrite_type(t, remap), - TypeDefKind::Map(k, v) => { - rewrite_type(k, remap); - rewrite_type(v, remap); - } - TypeDefKind::Result(r) => { - if let Some(ty) = &mut r.ok { - rewrite_type(ty, remap); - } - if let Some(ty) = &mut r.err { - rewrite_type(ty, remap); - } - } - TypeDefKind::Future(t) | TypeDefKind::Stream(t) => { - if let Some(ty) = t { - rewrite_type(ty, remap); - } - } - TypeDefKind::Handle(Handle::Own(id) | Handle::Borrow(id)) => { - rewrite_type_id(id, remap); - } - TypeDefKind::Flags(_) - | TypeDefKind::Enum(_) - | TypeDefKind::Resource - | TypeDefKind::Unknown => {} - } -} - -fn rewrite_type_ids_in_func(func: &mut Function, remap: &HashMap) { - for param in func.params.iter_mut() { - rewrite_type(&mut param.ty, remap); - } - if let Some(ty) = &mut func.result { - rewrite_type(ty, remap); - } - if let Some(id) = func.kind.resource_mut() { - rewrite_type_id(id, remap); - } +/// Whether `name` is a fully-qualified `namespace:package/name` rather than a +/// plain kebab name. +fn is_qualified_name(name: &str) -> bool { + matches!( + ComponentName::new(name, 0).map(|n| matches!(n.kind(), ComponentNameKind::Interface(_))), + Ok(true) + ) } From 929138797371bf9fc97241c71644140f5a06f563 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 18 Aug 2026 15:40:05 -0400 Subject: [PATCH 05/12] Make types-only WIT packages round-trip through the binary format Previously a package containing only package-scope types (no interfaces or worlds) could be encoded but not decoded, because the package name is only recoverable from the fully-qualified export inside an interface's or world's component type and such a package has neither. Generalize the encoding for that case: each package-scope type is additionally bound with a self-referential `eq` import named by its fully-qualified `namespace:package/name`, placed right after the type's export so V2 format sniffing still sees a label as the first export. Later types reference the imported index so every import satisfies the component model's named-type validation rules. Packages that do have an interface or world encode byte-identically to before. Decoding detects a self-import by resolving its `eq` bound back to one of the package's own exported types and recovers the package name from the import, keeping foreign package-scope dependencies (whose bounds are restated definitions) on the existing path. The types-only bail is now only reachable for binaries produced before these imports existed. Signed-off-by: Yordis Prieto --- crates/wit-component/src/encoding/wit.rs | 25 ++++ crates/wit-component/src/lib.rs | 107 +++++++++++++++--- .../interfaces/package-scope-types-only.wat | 15 +++ .../interfaces/package-scope-types-only.wit | 15 +++ .../package-scope-types-only.wit.print | 16 +++ crates/wit-parser/src/decoding.rs | 75 ++++++++++-- 6 files changed, 228 insertions(+), 25 deletions(-) create mode 100644 crates/wit-component/tests/interfaces/package-scope-types-only.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-types-only.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-types-only.wit.print diff --git a/crates/wit-component/src/encoding/wit.rs b/crates/wit-component/src/encoding/wit.rs index eec548508a..9b877036a3 100644 --- a/crates/wit-component/src/encoding/wit.rs +++ b/crates/wit-component/src/encoding/wit.rs @@ -145,6 +145,16 @@ impl Encoder<'_> { // Encode package-scope types first so V2 sniffing still sees a Label // as the first export name when types are present. { + // Decoding recovers the package name from the fully-qualified + // export inside an interface's or world's component type. A + // package without either has no such export, so each of its types + // is additionally bound with a self-referential `eq` import whose + // fully-qualified name carries the package name. Later types then + // reference the imported index so that every import stays valid + // per the component model's named-type rules. + let pkg = &self.resolve.packages[self.package]; + let self_describe = pkg.interfaces.is_empty() && pkg.worlds.is_empty(); + let mut encoder = PackageTypeEncoder { component: &mut self.component, package: self.package, @@ -154,6 +164,21 @@ impl Encoder<'_> { }; for (_, &id) in self.resolve.packages[self.package].types.iter() { encoder.encode_valtype(self.resolve, &Type::Id(id))?; + + if self_describe { + let index = encoder.type_encoding_maps.id_to_index[&id]; + let ty = &self.resolve.types[id]; + let imported = encoder.component.import( + ComponentExternName { + name: package_type_name(self.resolve, id).into(), + implements: None, + version_suffix: None, + external_id: ty.external_id.clone().map(Into::into), + }, + ComponentTypeRef::Type(TypeBounds::Eq(index)), + ); + encoder.type_encoding_maps.id_to_index.insert(id, imported); + } } } diff --git a/crates/wit-component/src/lib.rs b/crates/wit-component/src/lib.rs index 907c511423..319daf47d8 100644 --- a/crates/wit-component/src/lib.rs +++ b/crates/wit-component/src/lib.rs @@ -367,23 +367,32 @@ type point-list = list; let wasm = encode(&resolve, pkg)?; let wat = wasmprinter::print_bytes(&wasm)?; - // `point` is exported first as a record... + // `point` is exported first as a record and, since this package has + // no interfaces or worlds, re-imported under its fully-qualified name + // so the package name is recoverable... assert!( wat.contains("(record (field \"x\" u32) (field \"y\" u32))") - && wat.contains("\"point\" (type 0)"), - "expected `point` to be exported as its own definition:\n{wat}" + && wat.contains("\"point\" (type 0)") + && wat.contains("(import \"local:demo/point\" (type (;2;) (eq 1)))"), + "expected `point` to be exported and self-imported:\n{wat}" ); - // ...and `point-list` is a `list` of that exported type, exported next. + // ...and `point-list` is a `list` of the imported `point`, exported + // next. assert!( - wat.contains("(type (;2;) (list 1))") && wat.contains("\"point-list\" (type 2)"), - "expected `point-list` to reference the exported `point`:\n{wat}" + wat.contains("(type (;3;) (list 2))") && wat.contains("\"point-list\" (type 3)"), + "expected `point-list` to reference the imported `point`:\n{wat}" ); wasmparser::Validator::new_with_features(WasmFeatures::all()).validate_all(&wasm)?; Ok(()) } + /// A package without interfaces or worlds has no fully-qualified export + /// to recover the package name from, so each of its types is additionally + /// bound with a self-referential `eq` import named by the type's + /// fully-qualified name, and decoding recovers the package name from + /// those imports. #[test] - fn package_scope_type_only_package_decode_fails() -> Result<()> { + fn package_scope_type_only_package_round_trips() -> Result<()> { let mut resolve = Resolve::new(); let pkg = resolve.push_str( "types-only.wit", @@ -394,21 +403,83 @@ record point { x: u32, y: u32, } + +type path = list; "#, )?; let wasm = encode(&resolve, pkg)?; - match crate::decode(&wasm) { - Ok(_) => panic!("type-only packages cannot round-trip"), - Err(err) => { - let msg = format!("{err:#}"); - assert!( - msg.contains("only package-scope types") - || msg.contains("no interfaces or worlds"), - "unexpected error: {msg}" - ); - } - } + wasmparser::Validator::new_with_features(WasmFeatures::all()).validate_all(&wasm)?; + + let wat = wasmprinter::print_bytes(&wasm)?; + assert!( + wat.contains("(import \"local:types/point\""), + "expected a self-import binding `point`:\n{wat}" + ); + assert!( + wat.contains("(import \"local:types/path\""), + "expected a self-import binding `path`:\n{wat}" + ); + + let decoded = crate::decode(&wasm)?; + let resolve = decoded.resolve(); + let package = &resolve.packages[decoded.package()]; + assert_eq!(package.name.to_string(), "local:types"); + assert_eq!(package.types.len(), 2); + assert!(package.types.contains_key("point")); + assert!(package.types.contains_key("path")); + assert!(package.interfaces.is_empty()); + assert!(package.worlds.is_empty()); + Ok(()) + } + + /// Same as above, but the types-only package also depends on a foreign + /// package-scope type: the self-imports and the foreign import coexist at + /// the package root and decoding tells them apart. + #[test] + fn package_scope_type_only_package_with_foreign_dep_round_trips() -> Result<()> { + let mut resolve = Resolve::new(); + resolve.push_str( + "types.wit", + r#" +package local:types; + +record point { + x: u32, + y: u32, +} +"#, + )?; + let pkg = resolve.push_str( + "consumer.wit", + r#" +package local:consumer; + +use local:types/point; + +record bin { + p: point, +} +"#, + )?; + + let wasm = encode(&resolve, pkg)?; + wasmparser::Validator::new_with_features(WasmFeatures::all()).validate_all(&wasm)?; + + let decoded = crate::decode(&wasm)?; + let resolve = decoded.resolve(); + let package = &resolve.packages[decoded.package()]; + assert_eq!(package.name.to_string(), "local:consumer"); + assert!(package.types.contains_key("bin")); + + // The foreign `point` stays owned by `local:types`. + let types_pkg = resolve + .packages + .iter() + .find(|(_, p)| p.name.to_string() == "local:types") + .map(|(_, p)| p) + .expect("foreign package should be present"); + assert!(types_pkg.types.contains_key("point")); Ok(()) } diff --git a/crates/wit-component/tests/interfaces/package-scope-types-only.wat b/crates/wit-component/tests/interfaces/package-scope-types-only.wat new file mode 100644 index 0000000000..d6d562f167 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-types-only.wat @@ -0,0 +1,15 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type 0)) + (import "local:demo/point" (type (;2;) (eq 1))) + (type (;3;) (enum "north" "south" "east" "west")) + (export (;4;) "direction" (type 3)) + (import "local:demo/direction" (type (;5;) (eq 4))) + (type (;6;) (list 2)) + (export (;7;) "path" (type 6)) + (import "local:demo/path" (type (;8;) (eq 7))) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-types-only.wit b/crates/wit-component/tests/interfaces/package-scope-types-only.wit new file mode 100644 index 0000000000..cb16bf27f6 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-types-only.wit @@ -0,0 +1,15 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +enum direction { + north, + south, + east, + west, +} + +type path = list; diff --git a/crates/wit-component/tests/interfaces/package-scope-types-only.wit.print b/crates/wit-component/tests/interfaces/package-scope-types-only.wit.print new file mode 100644 index 0000000000..9188689e15 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-types-only.wit.print @@ -0,0 +1,16 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +enum direction { + north, + south, + east, + west, +} + +type path = list; + diff --git a/crates/wit-parser/src/decoding.rs b/crates/wit-parser/src/decoding.rs index ed72c709c4..b692dd24a7 100644 --- a/crates/wit-parser/src/decoding.rs +++ b/crates/wit-parser/src/decoding.rs @@ -234,11 +234,24 @@ impl ComponentInfo { let export = match item { Extern::Export(e) => e, - // An import here names a package-scope type from another - // package that this package's own definitions depend on. It's - // necessarily foreign, so there's no local definition to - // reconcile it with yet. + // An import here either binds one of this package's own + // exported types under its fully-qualified name, which is how + // the package name of a package without interfaces or worlds + // is recovered, or it names a package-scope type from another + // package that this package's own definitions depend on. Extern::Import(import) => { + if let Some(owner) = + decoder.match_self_package_type_import(export_name, import, &fields)? + { + if let Some(prev) = pkg_name.as_ref() { + if *prev != owner { + bail!("item defined with mismatched package name") + } + } else { + pkg_name = Some(owner); + } + continue; + } decoder .register_package_type_import(export_name, import, None, &mut fields) .with_context(|| format!("failed to process import `{export_name}`"))?; @@ -325,10 +338,13 @@ impl ComponentInfo { types, } } else if !types.is_empty() { + // A package encoded with self-describing type imports never lands + // here; this is only reachable for binaries produced before those + // imports were emitted for packages without interfaces or worlds. bail!( - "WIT package with only package-scope types (no interfaces or worlds) \ - cannot be decoded; add an interface or world so the package name can \ - be recovered from the binary format" + "cannot recover the package name of this WIT package: it contains \ + only package-scope types and no fully-qualified names; re-encode \ + it with a newer version of wit-component" ); } else { bail!("no exported component type found"); @@ -707,6 +723,51 @@ impl WitPackageDecoder<'_> { /// already decoded for `package`, when the name belongs to it, or with a /// lazily created type otherwise. `package` is `None` where no local /// definition can be in scope yet, which makes the name necessarily foreign. + /// Detects a self-import: a root-level type import bound with `eq` to one + /// of this package's own exported types, named by its fully-qualified + /// `namespace:package/name`. + /// + /// Such an import is emitted for packages without interfaces or worlds, + /// where it is the only place the package name appears in the binary. + /// Returns the package name recovered from the import, or `None` when the + /// import is a foreign package-scope dependency instead. + fn match_self_package_type_import( + &mut self, + name: &str, + item: &ComponentItem, + fields: &PackageFields<'_>, + ) -> Result> { + let parsed = self.parse_component_name(name)?; + let owner_name = match parsed.kind() { + ComponentNameKind::Interface(id) => id, + _ => return Ok(None), + }; + let type_name = owner_name.interface().to_string(); + + let (referenced, created) = match item.ty { + ComponentEntityType::Type { + referenced, + created, + } => (referenced, created), + _ => return Ok(None), + }; + + // The import is a self-import only when its `eq` bound resolves to the + // same type that this package exported under the import's unqualified + // label. A foreign dependency's bound is a restated definition that no + // local export refers to, so it never matches. + let Some(prev) = self.find_alias(referenced) else { + return Ok(None); + }; + if fields.types.get(&type_name) != Some(&prev) { + return Ok(None); + } + + let owner = owner_name.to_package_name(item)?; + self.type_map.insert(created, prev); + Ok(Some(owner)) + } + fn register_package_type_import( &mut self, name: &str, From e2e9c27b05dfcc2fead3e9c185a5e28eef250ceb Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 18 Aug 2026 16:15:57 -0400 Subject: [PATCH 06/12] Add package-scope fixtures covering use combinations Cover the remaining matrix of package-scope types mixed with classic `use iface.{...}` projections: same-interface use, world-level use, resource projection use, and a foreign package-scope use alongside a local interface projection. Each fixture encodes both the instance import path and the package-scope `eq` type import on the same wrapping component-type and round-trips through encode/decode/print. Signed-off-by: Yordis Prieto --- .../interfaces/package-scope-mixed-use.wat | 77 +++++++++++++++++++ .../package-scope-mixed-use/consumer.wit | 21 +++++ .../consumer.wit.print | 24 ++++++ .../deps/types/types.wit | 6 ++ .../package-scope-with-iface-use.wat | 47 +++++++++++ .../package-scope-with-iface-use.wit | 20 +++++ .../package-scope-with-iface-use.wit.print | 22 ++++++ .../package-scope-with-resource-use.wat | 49 ++++++++++++ .../package-scope-with-resource-use.wit | 19 +++++ .../package-scope-with-resource-use.wit.print | 21 +++++ .../package-scope-with-world-use.wat | 45 +++++++++++ .../package-scope-with-world-use.wit | 17 ++++ .../package-scope-with-world-use.wit.print | 18 +++++ 13 files changed, 386 insertions(+) create mode 100644 crates/wit-component/tests/interfaces/package-scope-mixed-use.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-mixed-use/consumer.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-mixed-use/consumer.wit.print create mode 100644 crates/wit-component/tests/interfaces/package-scope-mixed-use/deps/types/types.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-with-iface-use.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-with-iface-use.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-with-iface-use.wit.print create mode 100644 crates/wit-component/tests/interfaces/package-scope-with-resource-use.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-with-resource-use.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-with-resource-use.wit.print create mode 100644 crates/wit-component/tests/interfaces/package-scope-with-world-use.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-with-world-use.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-with-world-use.wit.print diff --git a/crates/wit-component/tests/interfaces/package-scope-mixed-use.wat b/crates/wit-component/tests/interfaces/package-scope-mixed-use.wat new file mode 100644 index 0000000000..bd5b55dd5e --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-mixed-use.wat @@ -0,0 +1,77 @@ +(component + (type (;0;) + (component + (type (;0;) + (instance + (type (;0;) (enum "info" "debug")) + (export (;1;) "level" (type (eq 0))) + ) + ) + (export (;0;) "local:consumer/levels" (instance (type 0))) + ) + ) + (export (;1;) "levels" (type 0)) + (type (;2;) + (component + (type (;0;) + (instance + (type (;0;) (enum "info" "debug")) + (export (;1;) "level" (type (eq 0))) + ) + ) + (import "local:consumer/levels" (instance (;0;) (type 0))) + (alias export 0 "level" (type (;1;))) + (type (;2;) (record (field "x" u32) (field "y" u32))) + (import "local:types/point" (type (;3;) (eq 2))) + (type (;4;) + (instance + (alias outer 1 1 (type (;0;))) + (export (;1;) "level" (type (eq 0))) + (alias outer 1 3 (type (;2;))) + (type (;3;) (func (param "p" 2))) + (export (;0;) "move-to" (func (type 3))) + (type (;4;) (func (param "level" 1) (param "msg" string))) + (export (;1;) "log" (func (type 4))) + ) + ) + (export (;1;) "local:consumer/api" (instance (type 4))) + ) + ) + (export (;3;) "api" (type 2)) + (type (;4;) + (component + (type (;0;) + (component + (type (;0;) + (instance + (type (;0;) (enum "info" "debug")) + (export (;1;) "level" (type (eq 0))) + ) + ) + (import "local:consumer/levels" (instance (;0;) (type 0))) + (alias export 0 "level" (type (;1;))) + (type (;2;) (record (field "x" u32) (field "y" u32))) + (import "local:types/point" (type (;3;) (eq 2))) + (type (;4;) + (instance + (alias outer 1 1 (type (;0;))) + (export (;1;) "level" (type (eq 0))) + (alias outer 1 3 (type (;2;))) + (type (;3;) (func (param "p" 2))) + (export (;0;) "move-to" (func (type 3))) + (type (;4;) (func (param "level" 1) (param "msg" string))) + (export (;1;) "log" (func (type 4))) + ) + ) + (export (;1;) "local:consumer/api" (instance (type 4))) + ) + ) + (export (;0;) "local:consumer/w" (component (type 0))) + ) + ) + (export (;5;) "w" (type 4)) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-mixed-use/consumer.wit b/crates/wit-component/tests/interfaces/package-scope-mixed-use/consumer.wit new file mode 100644 index 0000000000..6d31b1ce22 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-mixed-use/consumer.wit @@ -0,0 +1,21 @@ +package local:consumer; + +use local:types/point; + +interface levels { + enum level { + info, + debug, + } +} + +interface api { + use levels.{level}; + + move-to: func(p: point); + log: func(level: level, msg: string); +} + +world w { + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-mixed-use/consumer.wit.print b/crates/wit-component/tests/interfaces/package-scope-mixed-use/consumer.wit.print new file mode 100644 index 0000000000..4ab94f7b07 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-mixed-use/consumer.wit.print @@ -0,0 +1,24 @@ +package local:consumer; + +use local:types/point; + +interface levels { + enum level { + info, + debug, + } +} + +interface api { + use levels.{level}; + + move-to: func(p: point); + + log: func(level: level, msg: string); +} + +world w { + import levels; + + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-mixed-use/deps/types/types.wit b/crates/wit-component/tests/interfaces/package-scope-mixed-use/deps/types/types.wit new file mode 100644 index 0000000000..a15e70a05a --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-mixed-use/deps/types/types.wit @@ -0,0 +1,6 @@ +package local:types; + +record point { + x: u32, + y: u32, +} diff --git a/crates/wit-component/tests/interfaces/package-scope-with-iface-use.wat b/crates/wit-component/tests/interfaces/package-scope-with-iface-use.wat new file mode 100644 index 0000000000..8059a11f02 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-with-iface-use.wat @@ -0,0 +1,47 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type 0)) + (type (;2;) + (component + (type (;0;) + (instance + (type (;0;) (enum "info" "debug")) + (export (;1;) "level" (type (eq 0))) + ) + ) + (export (;0;) "local:demo/types" (instance (type 0))) + ) + ) + (export (;3;) "types" (type 2)) + (type (;4;) + (component + (type (;0;) + (instance + (type (;0;) (enum "info" "debug")) + (export (;1;) "level" (type (eq 0))) + ) + ) + (import "local:demo/types" (instance (;0;) (type 0))) + (alias export 0 "level" (type (;1;))) + (type (;2;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (type (;3;) (eq 2))) + (type (;4;) + (instance + (alias outer 1 1 (type (;0;))) + (export (;1;) "level" (type (eq 0))) + (alias outer 1 3 (type (;2;))) + (type (;3;) (func (param "p" 2))) + (export (;0;) "move-to" (func (type 3))) + (type (;4;) (func (param "level" 1) (param "msg" string))) + (export (;1;) "log" (func (type 4))) + ) + ) + (export (;1;) "local:demo/api" (instance (type 4))) + ) + ) + (export (;5;) "api" (type 4)) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-with-iface-use.wit b/crates/wit-component/tests/interfaces/package-scope-with-iface-use.wit new file mode 100644 index 0000000000..2d5007b177 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-with-iface-use.wit @@ -0,0 +1,20 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +interface types { + enum level { + info, + debug, + } +} + +interface api { + use types.{level}; + + move-to: func(p: point); + log: func(level: level, msg: string); +} diff --git a/crates/wit-component/tests/interfaces/package-scope-with-iface-use.wit.print b/crates/wit-component/tests/interfaces/package-scope-with-iface-use.wit.print new file mode 100644 index 0000000000..e178ceb1ba --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-with-iface-use.wit.print @@ -0,0 +1,22 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +interface types { + enum level { + info, + debug, + } +} + +interface api { + use types.{level}; + + move-to: func(p: point); + + log: func(level: level, msg: string); +} + diff --git a/crates/wit-component/tests/interfaces/package-scope-with-resource-use.wat b/crates/wit-component/tests/interfaces/package-scope-with-resource-use.wat new file mode 100644 index 0000000000..4702f19cab --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-with-resource-use.wat @@ -0,0 +1,49 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type 0)) + (type (;2;) + (component + (type (;0;) + (instance + (export (;0;) "file" (type (sub resource))) + (type (;1;) (borrow 0)) + (type (;2;) (func (param "self" 1) (result string))) + (export (;0;) "[method]file.path" (func (type 2))) + ) + ) + (export (;0;) "local:demo/files" (instance (type 0))) + ) + ) + (export (;3;) "files" (type 2)) + (type (;4;) + (component + (type (;0;) + (instance + (export (;0;) "file" (type (sub resource))) + ) + ) + (import "local:demo/files" (instance (;0;) (type 0))) + (alias export 0 "file" (type (;1;))) + (type (;2;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (type (;3;) (eq 2))) + (type (;4;) + (instance + (alias outer 1 1 (type (;0;))) + (export (;1;) "file" (type (eq 0))) + (alias outer 1 3 (type (;2;))) + (type (;3;) (func (param "p" 2))) + (export (;0;) "move-to" (func (type 3))) + (type (;4;) (own 1)) + (type (;5;) (func (result 4))) + (export (;1;) "open" (func (type 5))) + ) + ) + (export (;1;) "local:demo/api" (instance (type 4))) + ) + ) + (export (;5;) "api" (type 4)) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-with-resource-use.wit b/crates/wit-component/tests/interfaces/package-scope-with-resource-use.wit new file mode 100644 index 0000000000..4fd036649e --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-with-resource-use.wit @@ -0,0 +1,19 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +interface files { + resource file { + path: func() -> string; + } +} + +interface api { + use files.{file}; + + move-to: func(p: point); + open: func() -> file; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-with-resource-use.wit.print b/crates/wit-component/tests/interfaces/package-scope-with-resource-use.wit.print new file mode 100644 index 0000000000..a243b3fab6 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-with-resource-use.wit.print @@ -0,0 +1,21 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +interface files { + resource file { + path: func() -> string; + } +} + +interface api { + use files.{file}; + + move-to: func(p: point); + + open: func() -> file; +} + diff --git a/crates/wit-component/tests/interfaces/package-scope-with-world-use.wat b/crates/wit-component/tests/interfaces/package-scope-with-world-use.wat new file mode 100644 index 0000000000..e5c2716ad5 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-with-world-use.wat @@ -0,0 +1,45 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type 0)) + (type (;2;) + (component + (type (;0;) + (instance + (type (;0;) string) + (export (;1;) "tag" (type (eq 0))) + ) + ) + (export (;0;) "local:demo/types" (instance (type 0))) + ) + ) + (export (;3;) "types" (type 2)) + (type (;4;) + (component + (type (;0;) + (component + (type (;0;) + (instance + (type (;0;) string) + (export (;1;) "tag" (type (eq 0))) + ) + ) + (import "local:demo/types" (instance (;0;) (type 0))) + (alias export 0 "tag" (type (;1;))) + (import "tag" (type (;2;) (eq 1))) + (type (;3;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (type (;4;) (eq 3))) + (type (;5;) (func (param "p" 4))) + (import "move-to" (func (;0;) (type 5))) + (type (;6;) (func (param "t" 2) (result 4))) + (export (;1;) "label" (func (type 6))) + ) + ) + (export (;0;) "local:demo/w" (component (type 0))) + ) + ) + (export (;5;) "w" (type 4)) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-with-world-use.wit b/crates/wit-component/tests/interfaces/package-scope-with-world-use.wit new file mode 100644 index 0000000000..0b62a14cb7 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-with-world-use.wit @@ -0,0 +1,17 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +interface types { + type tag = string; +} + +world w { + use types.{tag}; + + import move-to: func(p: point); + export label: func(t: tag) -> point; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-with-world-use.wit.print b/crates/wit-component/tests/interfaces/package-scope-with-world-use.wit.print new file mode 100644 index 0000000000..5090f92087 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-with-world-use.wit.print @@ -0,0 +1,18 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +interface types { + type tag = string; +} + +world w { + import types; + use types.{tag}; + import move-to: func(p: point); + + export label: func(t: tag) -> point; +} From f012d208e15d2b53c22873681808cd994d568e4e Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Wed, 19 Aug 2026 17:24:41 -0400 Subject: [PATCH 07/12] Add package-scope tests for `@external-id` and `implements` Package-scope types already carry `external_id` through parse/encode/decode, and worlds can mix `implements` labels with package-scope type imports, but neither combination had coverage. Add encode/decode fixtures for `@external-id` on package-scope types (including types-only self-imports), worlds that `implements` an interface while mentioning a package-scope type, and both together, plus parser UI and CLI round-trip/dummy-module checks. Signed-off-by: Yordis Prieto --- .../interfaces/package-scope-external-id.wat | 55 +++++++++ .../interfaces/package-scope-external-id.wit | 19 +++ .../package-scope-external-id.wit.print | 20 ++++ .../package-scope-implements-external-id.wat | 52 ++++++++ .../package-scope-implements-external-id.wit | 21 ++++ ...age-scope-implements-external-id.wit.print | 20 ++++ .../interfaces/package-scope-implements.wat | 112 ++++++++++++++++++ .../interfaces/package-scope-implements.wit | 27 +++++ .../package-scope-implements.wit.print | 28 +++++ .../package-scope-types-only-external-id.wat | 12 ++ .../package-scope-types-only-external-id.wit | 10 ++ ...age-scope-types-only-external-id.wit.print | 11 ++ .../tests/ui/package-scope-external-id.wit | 11 ++ .../ui/package-scope-external-id.wit.json | 57 +++++++++ tests/cli/dummy-package-scope-implements.wit | 20 ++++ .../roundtrip-package-scope-external-id.wit | 25 ++++ ...dtrip-package-scope-external-id.wit.stdout | 24 ++++ 17 files changed, 524 insertions(+) create mode 100644 crates/wit-component/tests/interfaces/package-scope-external-id.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-external-id.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-external-id.wit.print create mode 100644 crates/wit-component/tests/interfaces/package-scope-implements-external-id.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-implements-external-id.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-implements-external-id.wit.print create mode 100644 crates/wit-component/tests/interfaces/package-scope-implements.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-implements.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-implements.wit.print create mode 100644 crates/wit-component/tests/interfaces/package-scope-types-only-external-id.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-types-only-external-id.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-types-only-external-id.wit.print create mode 100644 crates/wit-parser/tests/ui/package-scope-external-id.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-external-id.wit.json create mode 100644 tests/cli/dummy-package-scope-implements.wit create mode 100644 tests/cli/roundtrip-package-scope-external-id.wit create mode 100644 tests/cli/roundtrip-package-scope-external-id.wit.stdout diff --git a/crates/wit-component/tests/interfaces/package-scope-external-id.wat b/crates/wit-component/tests/interfaces/package-scope-external-id.wat new file mode 100644 index 0000000000..f89be1d382 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-external-id.wat @@ -0,0 +1,55 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (external-id "pkg-point") (type 0)) + (type (;2;) (list 1)) + (export (;3;) "path" (external-id "pkg-path") (type 2)) + (type (;4;) + (component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (external-id "pkg-point") (type (;1;) (eq 0))) + (type (;2;) (list 1)) + (import "local:demo/path" (external-id "pkg-path") (type (;3;) (eq 2))) + (type (;4;) + (instance + (alias outer 1 1 (type (;0;))) + (type (;1;) (func (param "p" 0))) + (export (;0;) "move-to" (func (type 1))) + (alias outer 1 3 (type (;2;))) + (type (;3;) (func (result 2))) + (export (;1;) "trail" (func (type 3))) + ) + ) + (export (;0;) "local:demo/api" (instance (type 4))) + ) + ) + (export (;5;) "api" (type 4)) + (type (;6;) + (component + (type (;0;) + (component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (external-id "pkg-point") (type (;1;) (eq 0))) + (type (;2;) (list 1)) + (import "local:demo/path" (external-id "pkg-path") (type (;3;) (eq 2))) + (type (;4;) + (instance + (alias outer 1 1 (type (;0;))) + (type (;1;) (func (param "p" 0))) + (export (;0;) "move-to" (func (type 1))) + (alias outer 1 3 (type (;2;))) + (type (;3;) (func (result 2))) + (export (;1;) "trail" (func (type 3))) + ) + ) + (export (;0;) "local:demo/api" (instance (type 4))) + ) + ) + (export (;0;) "local:demo/w" (component (type 0))) + ) + ) + (export (;7;) "w" (type 6)) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-external-id.wit b/crates/wit-component/tests/interfaces/package-scope-external-id.wit new file mode 100644 index 0000000000..668a90b086 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-external-id.wit @@ -0,0 +1,19 @@ +package local:demo; + +@external-id("pkg-point") +record point { + x: u32, + y: u32, +} + +@external-id("pkg-path") +type path = list; + +interface api { + move-to: func(p: point); + trail: func() -> path; +} + +world w { + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-external-id.wit.print b/crates/wit-component/tests/interfaces/package-scope-external-id.wit.print new file mode 100644 index 0000000000..be8c1bf7c7 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-external-id.wit.print @@ -0,0 +1,20 @@ +package local:demo; + +@external-id("pkg-point") +record point { + x: u32, + y: u32, +} + +@external-id("pkg-path") +type path = list; + +interface api { + move-to: func(p: point); + + trail: func() -> path; +} + +world w { + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-implements-external-id.wat b/crates/wit-component/tests/interfaces/package-scope-implements-external-id.wat new file mode 100644 index 0000000000..3239b38598 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-implements-external-id.wat @@ -0,0 +1,52 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (external-id "pkg-point") (type 0)) + (type (;2;) + (component + (type (;0;) + (instance + (type (;0;) (option string)) + (type (;1;) (func (param "key" string) (result 0))) + (export (;0;) "get" (func (type 1))) + ) + ) + (export (;0;) "local:demo/store" (instance (type 0))) + ) + ) + (export (;3;) "store" (type 2)) + (type (;4;) + (component + (type (;0;) + (component + (type (;0;) + (instance + (type (;0;) (option string)) + (type (;1;) (func (param "key" string) (result 0))) + (export (;0;) "get" (func (type 1))) + ) + ) + (import "primary" (implements "local:demo/store") (instance (;0;) (type 0))) + (type (;1;) + (instance + (type (;0;) (option string)) + (type (;1;) (func (param "key" string) (result 0))) + (export (;0;) "get" (func (type 1))) + ) + ) + (import "backup" (implements "local:demo/store") (instance (;1;) (type 1))) + (type (;2;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (external-id "pkg-point") (type (;3;) (eq 2))) + (type (;4;) (func (param "p" 3))) + (import "move-to" (external-id "world-move") (func (;0;) (type 4))) + (export (;1;) "place" (func (type 4))) + ) + ) + (export (;0;) "local:demo/w" (component (type 0))) + ) + ) + (export (;5;) "w" (type 4)) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-implements-external-id.wit b/crates/wit-component/tests/interfaces/package-scope-implements-external-id.wit new file mode 100644 index 0000000000..b8b12df22e --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-implements-external-id.wit @@ -0,0 +1,21 @@ +package local:demo; + +@external-id("pkg-point") +record point { + x: u32, + y: u32, +} + +interface store { + get: func(key: string) -> option; +} + +world w { + import primary: store; + import backup: store; + + @external-id("world-move") + import move-to: func(p: point); + + export place: func(p: point); +} diff --git a/crates/wit-component/tests/interfaces/package-scope-implements-external-id.wit.print b/crates/wit-component/tests/interfaces/package-scope-implements-external-id.wit.print new file mode 100644 index 0000000000..ec4dbbb69c --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-implements-external-id.wit.print @@ -0,0 +1,20 @@ +package local:demo; + +@external-id("pkg-point") +record point { + x: u32, + y: u32, +} + +interface store { + get: func(key: string) -> option; +} + +world w { + import primary: store; + import backup: store; + @external-id("world-move") + import move-to: func(p: point); + + export place: func(p: point); +} diff --git a/crates/wit-component/tests/interfaces/package-scope-implements.wat b/crates/wit-component/tests/interfaces/package-scope-implements.wat new file mode 100644 index 0000000000..07d433185c --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-implements.wat @@ -0,0 +1,112 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type 0)) + (type (;2;) + (component + (type (;0;) + (instance + (type (;0;) (option string)) + (type (;1;) (func (param "key" string) (result 0))) + (export (;0;) "get" (func (type 1))) + (type (;2;) (func (param "key" string) (param "value" string))) + (export (;1;) "set" (func (type 2))) + ) + ) + (export (;0;) "local:demo/store" (instance (type 0))) + ) + ) + (export (;3;) "store" (type 2)) + (type (;4;) + (component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (type (;1;) (eq 0))) + (type (;2;) + (instance + (alias outer 1 1 (type (;0;))) + (type (;1;) (func (param "p" 0))) + (export (;0;) "move-to" (func (type 1))) + ) + ) + (export (;0;) "local:demo/api" (instance (type 2))) + ) + ) + (export (;5;) "api" (type 4)) + (type (;6;) + (component + (type (;0;) + (component + (type (;0;) + (instance + (type (;0;) (option string)) + (type (;1;) (func (param "key" string) (result 0))) + (export (;0;) "get" (func (type 1))) + (type (;2;) (func (param "key" string) (param "value" string))) + (export (;1;) "set" (func (type 2))) + ) + ) + (import "primary" (implements "local:demo/store") (instance (;0;) (type 0))) + (type (;1;) + (instance + (type (;0;) (option string)) + (type (;1;) (func (param "key" string) (result 0))) + (export (;0;) "get" (func (type 1))) + (type (;2;) (func (param "key" string) (param "value" string))) + (export (;1;) "set" (func (type 2))) + ) + ) + (import "backup" (implements "local:demo/store") (instance (;1;) (type 1))) + (type (;2;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (type (;3;) (eq 2))) + (type (;4;) (func (param "p" 3))) + (import "move-to" (func (;0;) (type 4))) + ) + ) + (export (;0;) "local:demo/multi-import" (component (type 0))) + ) + ) + (export (;7;) "multi-import" (type 6)) + (type (;8;) + (component + (type (;0;) + (component + (type (;0;) + (instance + (type (;0;) (option string)) + (type (;1;) (func (param "key" string) (result 0))) + (export (;0;) "get" (func (type 1))) + (type (;2;) (func (param "key" string) (param "value" string))) + (export (;1;) "set" (func (type 2))) + ) + ) + (import "local:demo/store" (instance (;0;) (type 0))) + (type (;1;) + (instance + (type (;0;) (option string)) + (type (;1;) (func (param "key" string) (result 0))) + (export (;0;) "get" (func (type 1))) + (type (;2;) (func (param "key" string) (param "value" string))) + (export (;1;) "set" (func (type 2))) + ) + ) + (import "cache" (implements "local:demo/store") (instance (;1;) (type 1))) + (type (;2;) (record (field "x" u32) (field "y" u32))) + (import "local:demo/point" (type (;3;) (eq 2))) + (type (;4;) + (instance + (alias outer 1 3 (type (;0;))) + (type (;1;) (func (param "p" 0))) + (export (;0;) "move-to" (func (type 1))) + ) + ) + (export (;2;) "local:demo/api" (instance (type 4))) + ) + ) + (export (;0;) "local:demo/mixed" (component (type 0))) + ) + ) + (export (;9;) "mixed" (type 8)) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-implements.wit b/crates/wit-component/tests/interfaces/package-scope-implements.wit new file mode 100644 index 0000000000..2ef35cc494 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-implements.wit @@ -0,0 +1,27 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +interface store { + get: func(key: string) -> option; + set: func(key: string, value: string); +} + +interface api { + move-to: func(p: point); +} + +world multi-import { + import primary: store; + import backup: store; + import move-to: func(p: point); +} + +world mixed { + import store; + import cache: store; + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-implements.wit.print b/crates/wit-component/tests/interfaces/package-scope-implements.wit.print new file mode 100644 index 0000000000..029a99c8fd --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-implements.wit.print @@ -0,0 +1,28 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +interface store { + get: func(key: string) -> option; + + set: func(key: string, value: string); +} + +interface api { + move-to: func(p: point); +} + +world multi-import { + import primary: store; + import backup: store; + import move-to: func(p: point); +} +world mixed { + import store; + import cache: store; + + export api; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-types-only-external-id.wat b/crates/wit-component/tests/interfaces/package-scope-types-only-external-id.wat new file mode 100644 index 0000000000..4f9c1deb55 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-types-only-external-id.wat @@ -0,0 +1,12 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (external-id "pkg-point") (type 0)) + (import "local:demo/point" (external-id "pkg-point") (type (;2;) (eq 1))) + (type (;3;) (list 2)) + (export (;4;) "path" (external-id "pkg-path") (type 3)) + (import "local:demo/path" (external-id "pkg-path") (type (;5;) (eq 4))) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-types-only-external-id.wit b/crates/wit-component/tests/interfaces/package-scope-types-only-external-id.wit new file mode 100644 index 0000000000..77ce56c8b1 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-types-only-external-id.wit @@ -0,0 +1,10 @@ +package local:demo; + +@external-id("pkg-point") +record point { + x: u32, + y: u32, +} + +@external-id("pkg-path") +type path = list; diff --git a/crates/wit-component/tests/interfaces/package-scope-types-only-external-id.wit.print b/crates/wit-component/tests/interfaces/package-scope-types-only-external-id.wit.print new file mode 100644 index 0000000000..a6dccd50eb --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-types-only-external-id.wit.print @@ -0,0 +1,11 @@ +package local:demo; + +@external-id("pkg-point") +record point { + x: u32, + y: u32, +} + +@external-id("pkg-path") +type path = list; + diff --git a/crates/wit-parser/tests/ui/package-scope-external-id.wit b/crates/wit-parser/tests/ui/package-scope-external-id.wit new file mode 100644 index 0000000000..a0e2cfccad --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-external-id.wit @@ -0,0 +1,11 @@ +package local:demo; + +@external-id("pkg-point") +record point { + x: u32, + y: u32, +} + +interface api { + move-to: func(p: point); +} diff --git a/crates/wit-parser/tests/ui/package-scope-external-id.wit.json b/crates/wit-parser/tests/ui/package-scope-external-id.wit.json new file mode 100644 index 0000000000..104eb49328 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-external-id.wit.json @@ -0,0 +1,57 @@ +{ + "worlds": [], + "interfaces": [ + { + "name": "api", + "types": {}, + "functions": { + "move-to": { + "name": "move-to", + "kind": "freestanding", + "params": [ + { + "name": "p", + "type": 0 + } + ] + } + }, + "package": 0 + } + ], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + }, + "external_id": "pkg-point" + } + ], + "packages": [ + { + "name": "local:demo", + "interfaces": { + "api": 0 + }, + "worlds": {}, + "types": { + "point": 0 + } + } + ] +} \ No newline at end of file diff --git a/tests/cli/dummy-package-scope-implements.wit b/tests/cli/dummy-package-scope-implements.wit new file mode 100644 index 0000000000..4d34d4faef --- /dev/null +++ b/tests/cli/dummy-package-scope-implements.wit @@ -0,0 +1,20 @@ +// RUN: component embed % --dummy-names legacy | \ +// component new | \ +// validate -f cm-implements + +package local:demo; + +record point { + x: u32, + y: u32, +} + +interface store { + get: func(key: string) -> option; +} + +world w { + import primary: store; + import backup: store; + import move-to: func(p: point); +} diff --git a/tests/cli/roundtrip-package-scope-external-id.wit b/tests/cli/roundtrip-package-scope-external-id.wit new file mode 100644 index 0000000000..e642e940d6 --- /dev/null +++ b/tests/cli/roundtrip-package-scope-external-id.wit @@ -0,0 +1,25 @@ +// RUN: component wit % + +package local:demo; + +@external-id("pkg-point") +record point { + x: u32, + y: u32, +} + +@external-id("pkg-path") +type path = list; + +interface api { + @external-id("iface-move") + move-to: func(p: point); +} + +world w { + @external-id("world-api") + export api; + + @external-id("world-move") + import move-to: func(p: point); +} diff --git a/tests/cli/roundtrip-package-scope-external-id.wit.stdout b/tests/cli/roundtrip-package-scope-external-id.wit.stdout new file mode 100644 index 0000000000..53f94e03b2 --- /dev/null +++ b/tests/cli/roundtrip-package-scope-external-id.wit.stdout @@ -0,0 +1,24 @@ +/// RUN: component wit % +package local:demo; + +@external-id("pkg-point") +record point { + x: u32, + y: u32, +} + +@external-id("pkg-path") +type path = list; + +interface api { + @external-id("iface-move") + move-to: func(p: point); +} + +world w { + @external-id("world-move") + import move-to: func(p: point); + + @external-id("world-api") + export api; +} From ac787ded53f9248ce45ed04b420240c7a14b2d86 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 20 Aug 2026 02:53:06 -0400 Subject: [PATCH 08/12] Add package-scope tests for same-name types across merged worlds Two packages may each declare a package-scope type under the same local name, so merging their worlds has to keep the types distinct rather than unify them into one global name. Cover both the resolve-level outcome and the resulting encoding, which names each type by its fully-qualified name. Also pin down that merging worlds that export the same kebab name with differing result types succeeds and keeps the destination world's type, since `MergeMap::build_type_id` does not compare structure yet. Signed-off-by: Yordis Prieto --- crates/wit-component/src/lib.rs | 58 ++++++++ .../wit-parser/tests/package-scope-merge.rs | 133 ++++++++++++++++++ 2 files changed, 191 insertions(+) diff --git a/crates/wit-component/src/lib.rs b/crates/wit-component/src/lib.rs index 319daf47d8..adb4d56b35 100644 --- a/crates/wit-component/src/lib.rs +++ b/crates/wit-component/src/lib.rs @@ -625,6 +625,64 @@ world w { ); Ok(()) } + + /// Merging worlds from two packages that each declare a package-scope type + /// under the same local name keeps both types distinct. The merged world + /// encodes them as separate imports named by their fully-qualified names + /// rather than unifying them into one. + #[test] + fn package_scope_world_merge_keeps_qualified_type_imports() -> Result<()> { + let mut resolve = Resolve::new(); + let pkg1 = resolve.push_str( + "b1.wit", + r#" +package a:b1; + +record r { + a: u32, +} + +world w1 { + export f1: func() -> r; +} +"#, + )?; + let pkg2 = resolve.push_str( + "b2.wit", + r#" +package a:b2; + +record r { + a: f32, +} + +world w2 { + export f2: func() -> r; +} +"#, + )?; + + let w1 = resolve.packages[pkg1].worlds["w1"]; + let w2 = resolve.packages[pkg2].worlds["w2"]; + resolve.merge_worlds(w2, w1, &mut wit_parser::CloneMaps::default())?; + + let wasm = encode(&resolve, pkg1)?; + wasmparser::Validator::new_with_features(WasmFeatures::all()).validate_all(&wasm)?; + let wat = wasmprinter::print_bytes(&wasm)?; + assert!( + wat.contains("(import \"a:b1/r\"") && wat.contains("(record (field \"a\" u32))"), + "expected `a:b1/r` as a u32 record:\n{wat}" + ); + assert!( + wat.contains("(import \"a:b2/r\"") && wat.contains("(record (field \"a\" f32))"), + "expected `a:b2/r` as an f32 record:\n{wat}" + ); + assert!( + wat.contains("\"f1\"") && wat.contains("\"f2\""), + "merged world should export both functions:\n{wat}" + ); + Ok(()) + } } #[cfg(all(test, feature = "dummy-module"))] diff --git a/crates/wit-parser/tests/package-scope-merge.rs b/crates/wit-parser/tests/package-scope-merge.rs index 59e3797ce2..32d7161d5a 100644 --- a/crates/wit-parser/tests/package-scope-merge.rs +++ b/crates/wit-parser/tests/package-scope-merge.rs @@ -174,6 +174,139 @@ world wb { resolve.assert_valid(); } +/// Two packages may each declare a package-scope type under the same local +/// name. Merging their worlds does not unify those types: a package-scope name +/// is unique within its package, so the two stay distinct and keep their own +/// owners. +#[test] +fn package_scope_same_local_name_survives_merge_worlds() { + let mut resolve = Resolve::new(); + let pkg1 = resolve + .push_str( + "b1.wit", + r#" +package a:b1; + +record r { + a: u32, +} + +world w1 { + export f1: func() -> r; +} +"#, + ) + .unwrap(); + let pkg2 = resolve + .push_str( + "b2.wit", + r#" +package a:b2; + +record r { + a: f32, +} + +world w2 { + export f2: func() -> r; +} +"#, + ) + .unwrap(); + + let w1 = resolve.packages[pkg1].worlds["w1"]; + let w2 = resolve.packages[pkg2].worlds["w2"]; + let r1 = resolve.packages[pkg1].types["r"]; + let r2 = resolve.packages[pkg2].types["r"]; + assert_ne!(r1, r2); + + resolve + .merge_worlds(w2, w1, &mut CloneMaps::default()) + .unwrap(); + + assert_eq!(resolve.packages[pkg1].types["r"], r1); + assert_eq!(resolve.packages[pkg2].types["r"], r2); + assert_eq!(resolve.types[r1].owner, TypeOwner::Package(pkg1)); + assert_eq!(resolve.types[r2].owner, TypeOwner::Package(pkg2)); + + let f1 = match &resolve.worlds[w1].exports[&wit_parser::WorldKey::Name("f1".into())] { + wit_parser::WorldItem::Function(f) => f, + other => panic!("expected f1 to be a function, got {other:?}"), + }; + let f2 = match &resolve.worlds[w1].exports[&wit_parser::WorldKey::Name("f2".into())] { + wit_parser::WorldItem::Function(f) => f, + other => panic!("expected f2 to be a function, got {other:?}"), + }; + assert_eq!(f1.result, Some(wit_parser::Type::Id(r1))); + assert_eq!(f2.result, Some(wit_parser::Type::Id(r2))); + resolve.assert_valid(); +} + +/// When both worlds export the same kebab name, the shared item is merged +/// rather than added, and `MergeMap::build_type_id` does not compare type +/// structure (see its FIXME). So two `export f: func() -> r` worlds whose `r` +/// differs merge without error and `into`'s type wins. This is pre-existing +/// behavior for any named type, not specific to package scope; the test pins it +/// down so a future structural check is a deliberate change. +#[test] +fn package_scope_same_export_name_keeps_into_type_on_merge_worlds() { + let mut resolve = Resolve::new(); + let pkg1 = resolve + .push_str( + "b1.wit", + r#" +package a:b1; + +record r { + a: u32, +} + +world w1 { + export f: func() -> r; +} +"#, + ) + .unwrap(); + let pkg2 = resolve + .push_str( + "b2.wit", + r#" +package a:b2; + +record r { + a: f32, +} + +world w2 { + export f: func() -> r; +} +"#, + ) + .unwrap(); + + let w1 = resolve.packages[pkg1].worlds["w1"]; + let w2 = resolve.packages[pkg2].worlds["w2"]; + let r1 = resolve.packages[pkg1].types["r"]; + let r2 = resolve.packages[pkg2].types["r"]; + + resolve + .merge_worlds(w2, w1, &mut CloneMaps::default()) + .unwrap(); + + // `f` already existed in `w1`, so it was merged, not added, and kept + // pointing at `a:b1/r`. Both package-scope types still exist separately. + let f = match &resolve.worlds[w1].exports[&wit_parser::WorldKey::Name("f".into())] { + wit_parser::WorldItem::Function(f) => f, + other => panic!("expected f to be a function, got {other:?}"), + }; + assert_eq!(f.result, Some(wit_parser::Type::Id(r1))); + assert_eq!(resolve.packages[pkg1].types["r"], r1); + assert_eq!(resolve.packages[pkg2].types["r"], r2); + assert_eq!(resolve.types[r1].owner, TypeOwner::Package(pkg1)); + assert_eq!(resolve.types[r2].owner, TypeOwner::Package(pkg2)); + resolve.assert_valid(); +} + #[test] fn package_scope_type_vs_interface_merge_clash() { let mut into = Resolve::new(); From d788a3451065ce1fded802fb6b517030220963a9 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 20 Aug 2026 03:08:28 -0400 Subject: [PATCH 09/12] Move merged-world encoding test onto real WIT packages The inline `push_str` version could not exercise loading two packages through a `deps` directory, which is how a consumer actually hits this case. Use on-disk fixtures via `push_dir`, following the pattern of `tests/wit.rs`, so the test covers dependency resolution as well as the resulting encoding. Signed-off-by: Yordis Prieto --- crates/wit-component/src/lib.rs | 58 ------------------- .../tests/package-scope-world-merge.rs | 41 +++++++++++++ .../tests/package-scope-world-merge/b1.wit | 9 +++ .../package-scope-world-merge/deps/b2/b2.wit | 9 +++ 4 files changed, 59 insertions(+), 58 deletions(-) create mode 100644 crates/wit-component/tests/package-scope-world-merge.rs create mode 100644 crates/wit-component/tests/package-scope-world-merge/b1.wit create mode 100644 crates/wit-component/tests/package-scope-world-merge/deps/b2/b2.wit diff --git a/crates/wit-component/src/lib.rs b/crates/wit-component/src/lib.rs index adb4d56b35..319daf47d8 100644 --- a/crates/wit-component/src/lib.rs +++ b/crates/wit-component/src/lib.rs @@ -625,64 +625,6 @@ world w { ); Ok(()) } - - /// Merging worlds from two packages that each declare a package-scope type - /// under the same local name keeps both types distinct. The merged world - /// encodes them as separate imports named by their fully-qualified names - /// rather than unifying them into one. - #[test] - fn package_scope_world_merge_keeps_qualified_type_imports() -> Result<()> { - let mut resolve = Resolve::new(); - let pkg1 = resolve.push_str( - "b1.wit", - r#" -package a:b1; - -record r { - a: u32, -} - -world w1 { - export f1: func() -> r; -} -"#, - )?; - let pkg2 = resolve.push_str( - "b2.wit", - r#" -package a:b2; - -record r { - a: f32, -} - -world w2 { - export f2: func() -> r; -} -"#, - )?; - - let w1 = resolve.packages[pkg1].worlds["w1"]; - let w2 = resolve.packages[pkg2].worlds["w2"]; - resolve.merge_worlds(w2, w1, &mut wit_parser::CloneMaps::default())?; - - let wasm = encode(&resolve, pkg1)?; - wasmparser::Validator::new_with_features(WasmFeatures::all()).validate_all(&wasm)?; - let wat = wasmprinter::print_bytes(&wasm)?; - assert!( - wat.contains("(import \"a:b1/r\"") && wat.contains("(record (field \"a\" u32))"), - "expected `a:b1/r` as a u32 record:\n{wat}" - ); - assert!( - wat.contains("(import \"a:b2/r\"") && wat.contains("(record (field \"a\" f32))"), - "expected `a:b2/r` as an f32 record:\n{wat}" - ); - assert!( - wat.contains("\"f1\"") && wat.contains("\"f2\""), - "merged world should export both functions:\n{wat}" - ); - Ok(()) - } } #[cfg(all(test, feature = "dummy-module"))] diff --git a/crates/wit-component/tests/package-scope-world-merge.rs b/crates/wit-component/tests/package-scope-world-merge.rs new file mode 100644 index 0000000000..25d36018b1 --- /dev/null +++ b/crates/wit-component/tests/package-scope-world-merge.rs @@ -0,0 +1,41 @@ +//! Encoding of a world merged from two packages that each declare a +//! package-scope type under the same local name. + +use anyhow::Result; +use wasmparser::WasmFeatures; +use wit_parser::{CloneMaps, Resolve}; + +/// A package-scope name is unique within its package, so merging the worlds +/// keeps the two `r` types distinct. Each is encoded as its own `eq` import +/// named by its fully-qualified name rather than being unified into one. +#[test] +fn same_local_type_name_keeps_qualified_imports() -> Result<()> { + let mut resolve = Resolve::new(); + let (pkg1, _) = resolve.push_dir("tests/package-scope-world-merge")?; + let pkg2 = resolve + .packages + .iter() + .find_map(|(id, pkg)| (pkg.name.namespace == "a" && pkg.name.name == "b2").then_some(id)) + .unwrap(); + + let w1 = resolve.packages[pkg1].worlds["w1"]; + let w2 = resolve.packages[pkg2].worlds["w2"]; + resolve.merge_worlds(w2, w1, &mut CloneMaps::default())?; + + let wasm = wit_component::encode(&resolve, pkg1)?; + wasmparser::Validator::new_with_features(WasmFeatures::all()).validate_all(&wasm)?; + let wat = wasmprinter::print_bytes(&wasm)?; + assert!( + wat.contains("(import \"a:b1/r\"") && wat.contains("(record (field \"a\" u32))"), + "expected `a:b1/r` as a u32 record:\n{wat}" + ); + assert!( + wat.contains("(import \"a:b2/r\"") && wat.contains("(record (field \"a\" f32))"), + "expected `a:b2/r` as an f32 record:\n{wat}" + ); + assert!( + wat.contains("\"f1\"") && wat.contains("\"f2\""), + "merged world should export both functions:\n{wat}" + ); + Ok(()) +} diff --git a/crates/wit-component/tests/package-scope-world-merge/b1.wit b/crates/wit-component/tests/package-scope-world-merge/b1.wit new file mode 100644 index 0000000000..6bfe906942 --- /dev/null +++ b/crates/wit-component/tests/package-scope-world-merge/b1.wit @@ -0,0 +1,9 @@ +package a:b1; + +record r { + a: u32, +} + +world w1 { + export f1: func() -> r; +} diff --git a/crates/wit-component/tests/package-scope-world-merge/deps/b2/b2.wit b/crates/wit-component/tests/package-scope-world-merge/deps/b2/b2.wit new file mode 100644 index 0000000000..386eecfca1 --- /dev/null +++ b/crates/wit-component/tests/package-scope-world-merge/deps/b2/b2.wit @@ -0,0 +1,9 @@ +package a:b2; + +record r { + a: f32, +} + +world w2 { + export f2: func() -> r; +} From dbbfb02c403f443f59913d7130232a8c78f9b975 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 17 Sep 2026 22:40:17 -0400 Subject: [PATCH 10/12] fix(wit-parser): avoid panic on aliases of package-scope types Aliasing a package-scope type inside an interface or world is valid WIT that only became expressible with this feature, so the type dependency walk had never seen a package owner and treated it as unreachable. Users writing correct WIT got an internal error instead of a result. Signed-off-by: Yordis Prieto --- crates/wit-parser/src/resolve/mod.rs | 1 + .../ui/package-scope-interface-alias.wit | 19 ++++ .../ui/package-scope-interface-alias.wit.json | 99 +++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 crates/wit-parser/tests/ui/package-scope-interface-alias.wit create mode 100644 crates/wit-parser/tests/ui/package-scope-interface-alias.wit.json diff --git a/crates/wit-parser/src/resolve/mod.rs b/crates/wit-parser/src/resolve/mod.rs index b35bfd57fd..35193ec0f3 100644 --- a/crates/wit-parser/src/resolve/mod.rs +++ b/crates/wit-parser/src/resolve/mod.rs @@ -1635,6 +1635,7 @@ impl Resolve { } else { match other.owner { TypeOwner::Interface(id) => Some(id), + TypeOwner::Package(_) => None, _ => unreachable!(), } } diff --git a/crates/wit-parser/tests/ui/package-scope-interface-alias.wit b/crates/wit-parser/tests/ui/package-scope-interface-alias.wit new file mode 100644 index 0000000000..63b9fe57ae --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-interface-alias.wit @@ -0,0 +1,19 @@ +package local:demo; + +record point { + x: u32, + y: u32, +} + +interface api { + type pt = point; + + get: func() -> pt; +} + +world w { + type wpt = point; + + import api; + export run: func() -> wpt; +} diff --git a/crates/wit-parser/tests/ui/package-scope-interface-alias.wit.json b/crates/wit-parser/tests/ui/package-scope-interface-alias.wit.json new file mode 100644 index 0000000000..8837fab987 --- /dev/null +++ b/crates/wit-parser/tests/ui/package-scope-interface-alias.wit.json @@ -0,0 +1,99 @@ +{ + "worlds": [ + { + "name": "w", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + }, + "wpt": { + "type": 2 + } + }, + "exports": { + "run": { + "function": { + "name": "run", + "kind": "freestanding", + "params": [], + "result": 2 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "api", + "types": { + "pt": 1 + }, + "functions": { + "get": { + "name": "get", + "kind": "freestanding", + "params": [], + "result": 1 + } + }, + "package": 0 + } + ], + "types": [ + { + "name": "point", + "kind": { + "record": { + "fields": [ + { + "name": "x", + "type": "u32" + }, + { + "name": "y", + "type": "u32" + } + ] + } + }, + "owner": { + "package": 0 + } + }, + { + "name": "pt", + "kind": { + "type": 0 + }, + "owner": { + "interface": 0 + } + }, + { + "name": "wpt", + "kind": { + "type": 0 + }, + "owner": { + "world": 0 + } + } + ], + "packages": [ + { + "name": "local:demo", + "interfaces": { + "api": 0 + }, + "worlds": { + "w": 0 + }, + "types": { + "point": 0 + } + } + ] +} \ No newline at end of file From 147529409286541d2ce5a4cbd35d32032054fe95 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 17 Sep 2026 22:40:17 -0400 Subject: [PATCH 11/12] fix(wit-component): emit reparseable WIT for aliases of package-scope types The toplevel `use name as alias;` form is only accepted at package scope, so emitting it inside an interface or world body produced output that could not be fed back in. The printer is expected to round-trip, and the existing fixtures could not catch this because none aliased a package-scope type from within an interface or world. Signed-off-by: Yordis Prieto --- crates/wit-component/src/printing.rs | 8 ++++ .../package-scope-interface-alias.wat | 48 +++++++++++++++++++ .../package-scope-interface-alias.wit | 19 ++++++++ .../package-scope-interface-alias.wit.print | 20 ++++++++ 4 files changed, 95 insertions(+) create mode 100644 crates/wit-component/tests/interfaces/package-scope-interface-alias.wat create mode 100644 crates/wit-component/tests/interfaces/package-scope-interface-alias.wit create mode 100644 crates/wit-component/tests/interfaces/package-scope-interface-alias.wit.print diff --git a/crates/wit-component/src/printing.rs b/crates/wit-component/src/printing.rs index 9b49e15e55..e62796e26a 100644 --- a/crates/wit-component/src/printing.rs +++ b/crates/wit-component/src/printing.rs @@ -266,6 +266,14 @@ impl WitPrinter { match other.owner { TypeOwner::None => {} + // A package-scope type is visible by name throughout its + // own package, so an interface- or world-local alias of one + // is printed as a `type` declaration. The toplevel `use` + // form below is only valid at package scope: an interface + // body requires the `path.{..}` form, so printing a `use` + // here would emit WIT that cannot be reparsed. + TypeOwner::Package(_) if !matches!(owner, TypeOwner::Package(_)) => {} + // `use` is only applicable when the owner of the current // set of types is different than the owner of `other`. Once // this is detected `types_to_import` is going to get diff --git a/crates/wit-component/tests/interfaces/package-scope-interface-alias.wat b/crates/wit-component/tests/interfaces/package-scope-interface-alias.wat new file mode 100644 index 0000000000..5dcd4d1720 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-interface-alias.wat @@ -0,0 +1,48 @@ +(component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (export (;1;) "point" (type 0)) + (type (;2;) + (component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "foo:foo/point" (type (;1;) (eq 0))) + (type (;2;) + (instance + (alias outer 1 1 (type (;0;))) + (export (;1;) "pt" (type (eq 0))) + (type (;2;) (func (result 1))) + (export (;0;) "get" (func (type 2))) + ) + ) + (export (;0;) "foo:foo/api" (instance (type 2))) + ) + ) + (export (;3;) "api" (type 2)) + (type (;4;) + (component + (type (;0;) + (component + (type (;0;) (record (field "x" u32) (field "y" u32))) + (import "foo:foo/point" (type (;1;) (eq 0))) + (type (;2;) + (instance + (alias outer 1 1 (type (;0;))) + (export (;1;) "pt" (type (eq 0))) + (type (;2;) (func (result 1))) + (export (;0;) "get" (func (type 2))) + ) + ) + (import "foo:foo/api" (instance (;0;) (type 2))) + (import "wpt" (type (;3;) (eq 1))) + (type (;4;) (func (result 3))) + (export (;0;) "run" (func (type 4))) + ) + ) + (export (;0;) "foo:foo/w" (component (type 0))) + ) + ) + (export (;5;) "w" (type 4)) + (@custom "package-docs" "\01{}") + (@producers + (processed-by "wit-component" "$CARGO_PKG_VERSION") + ) +) diff --git a/crates/wit-component/tests/interfaces/package-scope-interface-alias.wit b/crates/wit-component/tests/interfaces/package-scope-interface-alias.wit new file mode 100644 index 0000000000..e744f2709b --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-interface-alias.wit @@ -0,0 +1,19 @@ +package foo:foo; + +record point { + x: u32, + y: u32, +} + +interface api { + type pt = point; + + get: func() -> pt; +} + +world w { + type wpt = point; + + import api; + export run: func() -> wpt; +} diff --git a/crates/wit-component/tests/interfaces/package-scope-interface-alias.wit.print b/crates/wit-component/tests/interfaces/package-scope-interface-alias.wit.print new file mode 100644 index 0000000000..012d853750 --- /dev/null +++ b/crates/wit-component/tests/interfaces/package-scope-interface-alias.wit.print @@ -0,0 +1,20 @@ +package foo:foo; + +record point { + x: u32, + y: u32, +} + +interface api { + type pt = point; + + get: func() -> pt; +} + +world w { + import api; + + type wpt = point; + + export run: func() -> wpt; +} From ff057d538cea60b10825b3540de6dc5395a6d5bd Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 17 Sep 2026 23:01:02 -0400 Subject: [PATCH 12/12] fix(wit-encoder): avoid panic on interface aliases of package-scope types A package-scope type has no owning interface, so the conversion could not name it through a `use` and gave up with an expect. Aliasing one inside an interface is valid WIT that only became expressible with this feature, so no existing test covered it. Signed-off-by: Yordis Prieto --- crates/wit-encoder/src/from_parser.rs | 9 +++- .../wit-encoder/tests/package-scope-types.rs | 44 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/crates/wit-encoder/src/from_parser.rs b/crates/wit-encoder/src/from_parser.rs index aef28d05d1..414bf49962 100644 --- a/crates/wit-encoder/src/from_parser.rs +++ b/crates/wit-encoder/src/from_parser.rs @@ -198,7 +198,14 @@ impl<'a> Converter<'a> { let type_def = self.resolve.types.get(*type_id).unwrap(); let underlying_type_def = self.underlying_type_def(&type_def); - if underlying_type_def.owner == owner { + // A package-scope type is in scope by name for every interface in + // its own package, so an alias of one is declared locally. The + // `use` branch below can only name a type owned by another + // interface. + let declare_locally = underlying_type_def.owner == owner + || matches!(underlying_type_def.owner, wit_parser::TypeOwner::Package(_)); + + if declare_locally { if let Some(type_def) = self.convert_type_def(type_def, *type_id) { output.item(InterfaceItem::TypeDef(type_def)); } diff --git a/crates/wit-encoder/tests/package-scope-types.rs b/crates/wit-encoder/tests/package-scope-types.rs index 2a8906b004..ffba1cf212 100644 --- a/crates/wit-encoder/tests/package-scope-types.rs +++ b/crates/wit-encoder/tests/package-scope-types.rs @@ -300,3 +300,47 @@ world w { .to_string() ); } + +/// An interface-local alias of a package-scope type is declared in place. The +/// `use` form cannot name it, because a package-scope type has no owning +/// interface to name it from. +#[test] +fn interface_alias_of_package_scope_type() { + const ALIAS_WIT: &str = r#"package local:demo; + +record point { + x: u32, +} + +type indirect = point; + +interface api { + type pt = point; + type chained = indirect; + get: func() -> pt; + get-chained: func() -> chained; +} + +world w { + export api; +} +"#; + + let mut resolve = wit_parser::Resolve::new(); + resolve.push_str("demo.wit", ALIAS_WIT).unwrap(); + let packages = packages_from_parsed(&resolve); + let rendered = packages[0].to_string(); + assert!( + rendered.contains("type pt = point"), + "expected a local declaration rather than a `use`:\n{rendered}" + ); + assert!( + rendered.contains("type chained = indirect"), + "expected the alias chain to resolve to its package-scope link:\n{rendered}" + ); + + let mut resolve2 = wit_parser::Resolve::new(); + resolve2.push_str("demo.wit", &rendered).unwrap(); + let packages2 = packages_from_parsed(&resolve2); + assert_eq!(packages[0].to_string(), packages2[0].to_string()); +}