diff --git a/crates/wac-graph/src/encoding.rs b/crates/wac-graph/src/encoding.rs index a2903871..6847e99d 100644 --- a/crates/wac-graph/src/encoding.rs +++ b/crates/wac-graph/src/encoding.rs @@ -4,9 +4,9 @@ use petgraph::graph::NodeIndex; use std::borrow::Cow; use std::collections::HashMap; use wac_types::{ - CoreExtern, DefinedType, DefinedTypeId, Enum, Flags, FuncTypeId, InterfaceId, ItemKind, - ModuleTypeId, PrimitiveType, Record, ResourceId, Type, Types, UsedType, ValueType, Variant, - WorldId, + is_label, CoreExtern, DefinedType, DefinedTypeId, Enum, Flags, FuncTypeId, InterfaceId, + ItemKind, ModuleTypeId, PrimitiveType, Record, ResourceId, Type, Types, UsedType, ValueType, + Variant, WorldId, }; use wasm_encoder::{ Alias, ComponentBuilder, ComponentCoreTypeEncoder, ComponentExportKind, ComponentExternName, @@ -14,11 +14,10 @@ use wasm_encoder::{ ComponentValType, EntityType, GlobalType, InstanceType, MemoryType, ModuleType, TableType, TagKind, TagType, TypeBounds, }; -use wasmparser::names::{ComponentName, ComponentNameKind}; -/// Determines whether an instance import/export needs a component model +/// Determines whether an import/export needs a component model /// `(implements "…")` directive, and if so returns the interface name to put -/// inside it. +/// inside it. Only instance externs can carry the directive. /// /// An instance extern is encoded one of two ways depending on its name: /// @@ -33,21 +32,21 @@ use wasmparser::names::{ComponentName, ComponentNameKind}; /// different labels. Here this returns `Some("foo:bar/iface")`, i.e. the /// string the caller should encode as the `implements` directive. /// -/// `name` is the extern name (`primary`) and `interface` identifies the -/// instance's interface within `types`, whose own name (`foo:bar/iface`) is the +/// `name` is the extern name (`primary`) and `kind` is the extern's kind within +/// `types`; for an instance, its interface's own name (`foo:bar/iface`) is the /// returned value. pub(crate) fn implements_directive<'a>( types: &'a Types, name: &str, - interface: InterfaceId, + kind: ItemKind, ) -> Option<&'a str> { + let ItemKind::Instance(interface) = kind else { + return None; + }; + // Only a plain-name label is encoded with an `implements` directive; a // regular interface extern already names its interface directly. - let is_label = matches!( - ComponentName::new(name, 0).as_ref().map(|n| n.kind()), - Ok(ComponentNameKind::Label(_)) - ); - if !is_label { + if !is_label(name) { return None; } types[interface].id.as_deref() @@ -146,6 +145,9 @@ pub struct Scope { pub instances: IndexMap, /// The map of import/export name to their alias indexes. type_aliases: IndexMap, + /// The map of instance index and type export name to the index of the + /// type aliased from that export. + export_aliases: HashMap<(u32, String), u32>, /// The map of resource names to their encoded indexes. resources: IndexMap, /// The encodable for this scope. @@ -201,6 +203,24 @@ impl State { prev.encodable } + /// Aliases the type export `name` of the instance at index `instance`, + /// reusing the alias if the current scope already has one. + fn alias_type_export(&mut self, instance: u32, name: &str) -> u32 { + let key = (instance, name.to_string()); + if let Some(index) = self.current.export_aliases.get(&key) { + return *index; + } + + let index = self.current.encodable.type_count(); + self.current.encodable.alias(Alias::InstanceExport { + instance, + kind: ComponentExportKind::Type, + name, + }); + self.current.export_aliases.insert(key, index); + index + } + fn used_type_index(&mut self, name: &str) -> Option { if let Some(index) = self.current.type_aliases.get(name) { return Some(*index); @@ -245,12 +265,7 @@ impl<'a> TypeEncoder<'a> { (*kind == ItemKind::Type(ty)).then(|| (instance, name.clone())) }) })?; - let index = state.current.encodable.type_count(); - state.current.encodable.alias(Alias::InstanceExport { - instance, - kind: ComponentExportKind::Type, - name: &name, - }); + let index = state.alias_type_export(instance, &name); state.current.type_indexes.insert(ty, index); Some(index) } @@ -342,14 +357,9 @@ impl<'a> TypeEncoder<'a> { let interface = &self.0[used.interface]; let iid = interface.id.as_ref().expect("interface should have an id"); let instance = state.current.instances[iid]; - let index = state.current.encodable.type_count(); let export: &String = used.name.as_ref().unwrap_or(name); let kind = interface.exports.get(export).unwrap(); - state.current.encodable.alias(Alias::InstanceExport { - instance, - kind: ComponentExportKind::Type, - name: export, - }); + let index = state.alias_type_export(instance, export); log::debug!( "aliased {kind} export `{export}` of instance index {instance} ({iid}) to type index {index}", @@ -407,6 +417,46 @@ impl<'a> TypeEncoder<'a> { } } + /// Encodes the type of the import or export `name` of the given kind. + /// + /// An instance named by a plain-name label, such as an `(implements "I")` + /// extern, gets an instance type of its own, as wit-component encodes it, + /// rather than the cached type of its interface, unless its interface can + /// share one (see [`Self::shares_instance_type`]). + /// + /// Otherwise this defers to [`Self::ty`], which resolves `name` as a used + /// type if `resolve_used` is set. + pub fn extern_ty( + &self, + state: &mut State, + name: &str, + kind: ItemKind, + resolve_used: bool, + ) -> u32 { + match kind { + ItemKind::Instance(id) if is_label(name) && !self.shares_instance_type(id) => { + self.instance(state, id, false) + } + _ => self.ty(state, kind.ty(), resolve_used.then_some(name)), + } + } + + /// Determines whether externs of an interface can share one instance type. + /// + /// Decoders such as wit-parser give each type an instance type exports one + /// identity, so externs sharing an instance type that exports a type see + /// it more than once and are rejected. Resources the interface defines are + /// the exception, as they are fresh for each extern; types it uses from + /// another interface are not. + fn shares_instance_type(&self, id: InterfaceId) -> bool { + let interface = &self.0[id]; + interface.uses.is_empty() + && interface + .exports + .values() + .all(|kind| matches!(kind, ItemKind::Func(_) | ItemKind::Type(Type::Resource(_)))) + } + pub fn component(&self, state: &mut State, id: WorldId) -> u32 { log::debug!("encoding component type"); let world = &self.0[id]; @@ -758,7 +808,7 @@ impl<'a> TypeEncoder<'a> { log::debug!("encoding {kind} import `{name}`", kind = kind.desc(self.0)); let ty = kind.ty(); - let index = self.ty(state, ty, Some(name)); + let index = self.extern_ty(state, name, kind, true); match kind { ItemKind::Type(_) => { @@ -779,7 +829,7 @@ impl<'a> TypeEncoder<'a> { } ItemKind::Instance(id) => { let import_index = state.current.encodable.instance_count(); - let implements = implements_directive(self.0, name, id); + let implements = implements_directive(self.0, name, kind); let extern_name = ComponentExternName { name: Cow::Borrowed(name), implements: implements.map(Cow::Borrowed), @@ -885,14 +935,10 @@ impl<'a> TypeEncoder<'a> { kind = kind.desc(self.0) ); - let ty = kind.ty(); - let index = self.ty(state, ty, Some(name)); + let index = self.extern_ty(state, name, kind, true); // Preserve the `(implements "I")` directive when re-encoding an // instance export that uses a plain-name label. - let implements = match kind { - ItemKind::Instance(id) => implements_directive(self.0, name, id), - _ => None, - }; + let implements = implements_directive(self.0, name, kind); let extern_name = ComponentExternName { name: Cow::Borrowed(name), implements: implements.map(Cow::Borrowed), diff --git a/crates/wac-graph/src/graph.rs b/crates/wac-graph/src/graph.rs index 2b6d56d7..5631ea9f 100644 --- a/crates/wac-graph/src/graph.rs +++ b/crates/wac-graph/src/graph.rs @@ -1,4 +1,4 @@ -use crate::encoding::{State, TypeEncoder}; +use crate::encoding::{implements_directive, State, TypeEncoder}; use indexmap::IndexMap; use petgraph::{ dot::{Config, Dot}, @@ -16,8 +16,8 @@ use std::{ }; use thiserror::Error; use wac_types::{ - BorrowedKey, BorrowedPackageKey, DefinedType, ItemKind, Package, PackageKey, SubtypeChecker, - Type, TypeAggregator, Types, ValueType, + is_label, BorrowedKey, BorrowedPackageKey, DefinedType, ItemKind, Package, PackageKey, + SubtypeChecker, Type, TypeAggregator, Types, ValueType, }; use wasm_encoder::{ Alias, ComponentBuilder, ComponentExportKind, ComponentExternName, ComponentNameSection, @@ -1455,10 +1455,27 @@ impl<'a> CompositionGraphEncoder<'a> { .filter(|(_, n)| !matches!(self.0.graph[**n].kind, NodeKind::Definition)) { let index = state.node_indexes[node]; - let node = &self.0.graph[*node]; + let kind = self.0.graph[*node].item_kind; + // Preserve the `(implements "I")` directive of an instance + // exported under a plain-name label, if it came from a labelled + // import or export that had one; a directive isn't added to an + // instance that never had one. + let source = match &self.0.graph[*node].kind { + NodeKind::Import(name) => Some(name.as_str()), + NodeKind::Alias => self.0.get_alias_source(NodeId(*node)).map(|(_, e)| e), + _ => None, + }; + let implements = implements_directive(&self.0.types, name, kind) + .filter(|_| source.is_some_and(is_label)); + let extern_name = ComponentExternName { + name: Cow::Borrowed(name), + implements: implements.map(Cow::Borrowed), + external_id: None, + version_suffix: None, + }; state .builder() - .export(name, node.item_kind.into(), index, None); + .export(extern_name, kind.into(), index, None); } let mut builder = std::mem::take(state.builder()); @@ -1707,10 +1724,7 @@ impl<'a> CompositionGraphEncoder<'a> { // labels, so implements imports are exempt from the shared-dependency // deduplication below and carry an `implements` directive when // re-encoded. - let implements = match kind { - ItemKind::Instance(id) => crate::encoding::implements_directive(types, name, id), - _ => None, - }; + let implements = implements_directive(types, name, kind); // Check to see if this is an import of an interface that's already been // imported; this can happen based on importing of shared dependencies. @@ -1738,7 +1752,7 @@ impl<'a> CompositionGraphEncoder<'a> { ); // Encode the type and import - let ty = encoder.ty(state, kind.ty(), None); + let ty = encoder.extern_ty(state, name, kind, false); let extern_name = ComponentExternName { name: Cow::Borrowed(name), implements: implements.map(Cow::Borrowed), diff --git a/crates/wac-graph/tests/encoding.rs b/crates/wac-graph/tests/encoding.rs index 05102916..36ac4b3b 100644 --- a/crates/wac-graph/tests/encoding.rs +++ b/crates/wac-graph/tests/encoding.rs @@ -8,9 +8,12 @@ use std::{ fs::{self, File}, path::{Path, PathBuf}, }; -use wac_graph::{types::Package, CompositionGraph, EncodeOptions, NodeId, PackageId}; +use wac_graph::{ + types::{Interface, ItemKind, Package, Type, World}, + CompositionGraph, EncodeOptions, NodeId, PackageId, +}; use wit_component::{ComponentEncoder, StringEncoding}; -use wit_parser::Resolve; +use wit_parser::{decoding::DecodedWasm, Resolve, WorldId, WorldItem, WorldKey}; /// Represents a node to add to a composition graph. #[derive(Deserialize)] @@ -386,3 +389,293 @@ fn encoding() -> Result<()> { Ok(()) } + +/// Describes the labelled instance externs among `items` as +/// `label: I [functions]`, where `I` is the interface an `(implements "I")` +/// extern names, or `_` for an unnamed interface. +fn labelled_externs<'a>( + resolve: &Resolve, + items: impl IntoIterator, +) -> Vec { + items + .into_iter() + .filter_map(|(key, item)| { + let (WorldKey::Name(label), WorldItem::Interface { id, .. }) = (key, item) else { + return None; + }; + let interface = resolve.id_of(*id).unwrap_or_else(|| "_".to_string()); + let functions = resolve.interfaces[*id] + .functions + .keys() + .map(String::as_str) + .collect::>() + .join(", "); + Some(format!("{label}: {interface} [{functions}]")) + }) + .collect() +} + +/// Encodes the composition graph of a test case in `tests/graphs`, with its +/// components defined inline, and decodes it with wit-parser. +fn decode_test_case(test_case: &str) -> Result<(Resolve, WorldId)> { + let root = Path::new("tests/graphs").join(test_case); + let file = File::open(root.join("graph.json"))?; + let graph = + serde_json::from_reader::<_, GraphFile>(file)?.into_composition_graph(&root, test_case)?; + let bytes = graph.encode(EncodeOptions::default())?; + match wit_parser::decoding::decode(&bytes) + .with_context(|| format!("failed to decode the composition of `{test_case}`"))? + { + DecodedWasm::Component(resolve, world) => Ok((resolve, world)), + DecodedWasm::WitPackage(..) => bail!("expected `{test_case}` to decode as a component"), + } +} + +/// Tests that a composition with several `(implements "I")` imports of one +/// interface decodes with wit-parser, through both implicit imports and +/// explicit import nodes. +/// +/// Each labelled import needs an instance type of its own, as wit-component +/// encodes it: labels sharing one instance type share the types it exports, +/// which wit-parser's decoder rejects. +#[test] +fn implements_imports_decode() -> Result<()> { + let expected = [ + "primary: test:test/logger [log]", + "backup: test:test/logger [log]", + ]; + + let (resolve, world) = decode_test_case("implements-imports-defined-type")?; + assert_eq!( + labelled_externs(&resolve, &resolve.worlds[world].imports), + expected + ); + + let (resolve, world) = decode_test_case("implements-imports-resource")?; + assert_eq!( + labelled_externs(&resolve, &resolve.worlds[world].imports), + ["left: test:test/res [f]", "right: test:test/res [f]"] + ); + + // Import the consumer's labels through explicit import nodes instead. + let consumer = wat::parse_file("tests/graphs/implements-imports-defined-type/consumer.wat")?; + let mut graph = CompositionGraph::new(); + let package = Package::from_bytes("test:consumer", None, consumer, graph.types_mut())?; + let package = graph.register_package(package)?; + let instantiation = graph.instantiate(package); + let imports = graph.types()[graph[package].ty()].imports.clone(); + for (name, kind) in imports { + let import = graph.import(&name, kind)?; + graph.set_instantiation_argument(instantiation, &name, import)?; + } + + let bytes = graph.encode(EncodeOptions::default())?; + let DecodedWasm::Component(resolve, world) = wit_parser::decoding::decode(&bytes) + .context("failed to decode the composition with explicit imports")? + else { + bail!("expected the composition to decode as a component"); + }; + assert_eq!( + labelled_externs(&resolve, &resolve.worlds[world].imports), + expected + ); + + Ok(()) +} + +/// Tests that labels of one interface imported by different components stay +/// separate imports, each implementing its own version of the interface, and +/// that one label imported at compatible versions implements the highest. +#[test] +fn implements_labels_decode() -> Result<()> { + let (resolve, world) = decode_test_case("implements-labels-unmerged")?; + assert_eq!( + labelled_externs(&resolve, &resolve.worlds[world].imports), + [ + "primary: test:test/logger@0.2.0 [log]", + "backup: test:test/logger@0.2.1 [log, flush]", + ] + ); + + let (resolve, world) = decode_test_case("implements-label-semver-merge")?; + assert_eq!( + labelled_externs(&resolve, &resolve.worlds[world].imports), + ["primary: test:test/logger@0.2.1 [log, flush]"] + ); + + Ok(()) +} + +/// Tests that a composition re-exporting `(implements "I")` exports of an +/// instantiation keeps their `implements` directive, and that re-exporting an +/// interface export under a label doesn't add one. +#[test] +fn implements_composition_exports_decode() -> Result<()> { + let (resolve, world) = decode_test_case("implements-composition-exports")?; + assert_eq!( + labelled_externs(&resolve, &resolve.worlds[world].exports), + [ + "primary: test:test/logger [log]", + "backup: test:test/logger [log]", + ] + ); + + let (resolve, world) = decode_test_case("implements-satisfied")?; + assert_eq!( + labelled_externs(&resolve, &resolve.worlds[world].exports), + ["renamed: _ [do-something]"] + ); + + Ok(()) +} + +#[test] +fn reexport_preserves_source_implements_metadata() -> Result<()> { + let producer = wat::parse_str( + r#"(component + (type (instance + (type (func)) + (export "log" (func (type 0))))) + (import "test:test/logger" (instance (type 0))) + (export "plain" (instance 0)) + (export "annotated" (implements "test:test/logger") (instance 0)) + )"#, + )?; + let mut graph = CompositionGraph::new(); + let package = Package::from_bytes("test:producer", None, producer, graph.types_mut())?; + let package = graph.register_package(package)?; + let instance = graph.instantiate(package); + for name in ["plain", "annotated"] { + let alias = graph.alias_instance_export(instance, name)?; + graph.export(alias, name)?; + } + + for define_components in [true, false] { + let bytes = graph.encode(EncodeOptions { + define_components, + ..Default::default() + })?; + let types = wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all()) + .validate_all(&bytes)?; + assert_eq!( + types.component_item_for_export("plain").unwrap().implements, + None + ); + assert_eq!( + types + .component_item_for_export("annotated") + .unwrap() + .implements + .as_deref(), + Some("test:test/logger") + ); + + if define_components { + let DecodedWasm::Component(resolve, world) = wit_parser::decoding::decode(&bytes)? + else { + bail!("expected a component"); + }; + assert_eq!( + labelled_externs(&resolve, &resolve.worlds[world].exports), + ["plain: _ [log]", "annotated: test:test/logger [log]"] + ); + } else { + let import = types + .component_item_for_import("unlocked-dep=") + .unwrap(); + let wasmparser::component_types::ComponentEntityType::Component(id) = import.ty else { + bail!("expected a component import"); + }; + assert_eq!(types[id].exports["plain"].implements, None); + assert_eq!( + types[id].exports["annotated"].implements.as_deref(), + Some("test:test/logger") + ); + } + } + + Ok(()) +} + +/// Tests that a world definition with several labelled imports and exports of +/// one interface decodes with wit-parser, whether or not the interface is +/// named (i.e. the labels are `(implements "I")` externs). +/// +/// This encodes the labelled externs within a component type, where each +/// needs an instance type of its own just as in a composition's imports, +/// unless the interface only exports functions and its own resources. +#[test] +fn implements_world_decode() -> Result<()> { + let consumer = wat::parse_file("tests/graphs/implements-imports-defined-type/consumer.wat")?; + let mut graph = CompositionGraph::new(); + let package = Package::from_bytes("test:consumer", None, consumer, graph.types_mut())?; + + // Each set of labels refers to one interface, as in a component whose + // labels share one instance type. + let ItemKind::Instance(logger) = graph.types()[package.ty()].imports["primary"] else { + bail!("expected `primary` to be an instance"); + }; + let unnamed = Interface { + id: None, + ..graph.types()[logger].clone() + }; + let unnamed = graph.types_mut().add_interface(unnamed); + let consumer = wat::parse_file("tests/graphs/implements-imports-resource/consumer.wat")?; + let package = Package::from_bytes("test:resources", None, consumer, graph.types_mut())?; + let ItemKind::Instance(res) = graph.types()[package.ty()].imports["left"] else { + bail!("expected `left` to be an instance"); + }; + let labelled = |interface, labels: &[&str]| { + labels + .iter() + .map(move |l| (l.to_string(), ItemKind::Instance(interface))) + .collect::>() + }; + let world = graph.types_mut().add_world(World { + id: Some("test:app/service".to_string()), + uses: Default::default(), + imports: [ + labelled(logger, &["primary", "backup"]), + labelled(unnamed, &["first", "second"]), + labelled(res, &["left", "right"]), + ] + .concat() + .into_iter() + .collect(), + exports: [ + labelled(logger, &["audit", "trace"]), + labelled(res, &["out"]), + ] + .concat() + .into_iter() + .collect(), + }); + graph.define_type("service", Type::World(world))?; + + let bytes = graph.encode(EncodeOptions::default())?; + let (resolve, world) = + wit_parser::decoding::decode_world(&bytes).context("failed to decode the world")?; + let world = &resolve.worlds[world]; + + assert_eq!( + labelled_externs(&resolve, &world.imports), + [ + "primary: test:test/logger [log]", + "backup: test:test/logger [log]", + "first: _ [log]", + "second: _ [log]", + "left: test:test/res [f]", + "right: test:test/res [f]", + ] + ); + assert_eq!( + labelled_externs(&resolve, &world.exports), + [ + "audit: test:test/logger [log]", + "trace: test:test/logger [log]", + "out: test:test/res [f]", + ] + ); + + Ok(()) +} diff --git a/crates/wac-graph/tests/graphs/duplicate-imports/encoded.wat b/crates/wac-graph/tests/graphs/duplicate-imports/encoded.wat index 3128c902..b930fc93 100644 --- a/crates/wac-graph/tests/graphs/duplicate-imports/encoded.wat +++ b/crates/wac-graph/tests/graphs/duplicate-imports/encoded.wat @@ -35,17 +35,16 @@ ) ) (import "test:foo/i" (instance (;1;) (type 2))) - (alias export 0 "x" (type (;3;))) - (type (;4;) + (type (;3;) (instance - (alias outer 1 3 (type (;0;))) + (alias outer 1 1 (type (;0;))) (export (;1;) "x" (type (eq 0))) (type (;2;) (own 1)) (type (;3;) (func (result 2))) (export (;0;) "f" (func (type 3))) ) ) - (export (;2;) "test:foo/i" (instance (type 4))) + (export (;2;) "test:foo/i" (instance (type 3))) ) ) (import "unlocked-dep=" (component (;0;) (type 3))) diff --git a/crates/wac-graph/tests/graphs/implements-composition-exports/encoded.wat b/crates/wac-graph/tests/graphs/implements-composition-exports/encoded.wat new file mode 100644 index 00000000..ef5675ac --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-composition-exports/encoded.wat @@ -0,0 +1,30 @@ +(component + (type (;0;) + (component + (type (;0;) + (instance + (type (;0;) (enum "debug" "info")) + (export (;1;) "level" (type (eq 0))) + (type (;2;) (func (param "level" 1) (param "message" string))) + (export (;0;) "log" (func (type 2))) + ) + ) + (export (;0;) "primary" (implements "test:test/logger") (instance (type 0))) + (type (;1;) + (instance + (type (;0;) (enum "debug" "info")) + (export (;1;) "level" (type (eq 0))) + (type (;2;) (func (param "level" 1) (param "message" string))) + (export (;0;) "log" (func (type 2))) + ) + ) + (export (;1;) "backup" (implements "test:test/logger") (instance (type 1))) + ) + ) + (import "unlocked-dep=" (component (;0;) (type 0))) + (instance (;0;) (instantiate 0)) + (alias export 0 "primary" (instance (;1;))) + (alias export 0 "backup" (instance (;2;))) + (export (;3;) "primary" (implements "test:test/logger") (instance 1)) + (export (;4;) "backup" (implements "test:test/logger") (instance 2)) +) diff --git a/crates/wac-graph/tests/graphs/implements-composition-exports/graph.json b/crates/wac-graph/tests/graphs/implements-composition-exports/graph.json new file mode 100644 index 00000000..640409c8 --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-composition-exports/graph.json @@ -0,0 +1,14 @@ +{ + "packages": [ + { "name": "test:producer", "path": "producer.wit" } + ], + "nodes": [ + { "type": "instantiation", "package": 0 }, + { "type": "alias", "source": 0, "export": "primary" }, + { "type": "alias", "source": 0, "export": "backup" } + ], + "exports": [ + { "node": 1, "name": "primary" }, + { "node": 2, "name": "backup" } + ] +} diff --git a/crates/wac-graph/tests/graphs/implements-composition-exports/producer.wit b/crates/wac-graph/tests/graphs/implements-composition-exports/producer.wit new file mode 100644 index 00000000..2de727cf --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-composition-exports/producer.wit @@ -0,0 +1,11 @@ +package test:test; + +interface logger { + enum level { debug, info } + log: func(level: level, message: string); +} + +world producer { + export primary: logger; + export backup: logger; +} diff --git a/crates/wac-graph/tests/graphs/implements-imports-defined-type/consumer.wat b/crates/wac-graph/tests/graphs/implements-imports-defined-type/consumer.wat new file mode 100644 index 00000000..4a85d9da --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-imports-defined-type/consumer.wat @@ -0,0 +1,14 @@ +(component + (type (instance + (type (enum "debug" "info")) + (export "level" (type (eq 0))) + (type (func (param "level" 1) (param "message" string))) + (export "log" (func (type 2))))) + (import "primary" (implements "test:test/logger") (instance (type 0))) + (type (instance + (type (enum "debug" "info")) + (export "level" (type (eq 0))) + (type (func (param "level" 1) (param "message" string))) + (export "log" (func (type 2))))) + (import "backup" (implements "test:test/logger") (instance (type 1))) +) diff --git a/crates/wac-graph/tests/graphs/implements-imports-defined-type/encoded.wat b/crates/wac-graph/tests/graphs/implements-imports-defined-type/encoded.wat new file mode 100644 index 00000000..e31dedd2 --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-imports-defined-type/encoded.wat @@ -0,0 +1,48 @@ +(component + (type (;0;) + (instance + (type (;0;) (enum "debug" "info")) + (export (;1;) "level" (type (eq 0))) + (type (;2;) (func (param "level" 1) (param "message" string))) + (export (;0;) "log" (func (type 2))) + ) + ) + (import "primary" (implements "test:test/logger") (instance (;0;) (type 0))) + (type (;1;) + (instance + (type (;0;) (enum "debug" "info")) + (export (;1;) "level" (type (eq 0))) + (type (;2;) (func (param "level" 1) (param "message" string))) + (export (;0;) "log" (func (type 2))) + ) + ) + (import "backup" (implements "test:test/logger") (instance (;1;) (type 1))) + (type (;2;) + (component + (type (;0;) + (instance + (type (;0;) (enum "debug" "info")) + (export (;1;) "level" (type (eq 0))) + (type (;2;) (func (param "level" 1) (param "message" string))) + (export (;0;) "log" (func (type 2))) + ) + ) + (import "primary" (implements "test:test/logger") (instance (;0;) (type 0))) + (type (;1;) + (instance + (type (;0;) (enum "debug" "info")) + (export (;1;) "level" (type (eq 0))) + (type (;2;) (func (param "level" 1) (param "message" string))) + (export (;0;) "log" (func (type 2))) + ) + ) + (import "backup" (implements "test:test/logger") (instance (;1;) (type 1))) + ) + ) + (import "unlocked-dep=" (component (;0;) (type 2))) + (instance (;2;) (instantiate 0 + (with "primary" (instance 0)) + (with "backup" (instance 1)) + ) + ) +) diff --git a/crates/wac-graph/tests/graphs/implements-imports-defined-type/graph.json b/crates/wac-graph/tests/graphs/implements-imports-defined-type/graph.json new file mode 100644 index 00000000..3fc81179 --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-imports-defined-type/graph.json @@ -0,0 +1,14 @@ +{ + "packages": [ + { + "name": "test:consumer", + "path": "consumer.wat" + } + ], + "nodes": [ + { + "type": "instantiation", + "package": 0 + } + ] +} diff --git a/crates/wac-graph/tests/graphs/implements-imports-resource/consumer.wat b/crates/wac-graph/tests/graphs/implements-imports-resource/consumer.wat new file mode 100644 index 00000000..57123664 --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-imports-resource/consumer.wat @@ -0,0 +1,11 @@ +(component + (type (instance + (export "r" (type (sub resource))) + (export "r2" (type (eq 0))) + (type (own 0)) + (type (borrow 1)) + (type (func (param "a" 2) (param "b" 3))) + (export "f" (func (type 4))))) + (import "left" (implements "test:test/res") (instance (type 0))) + (import "right" (implements "test:test/res") (instance (type 0))) +) diff --git a/crates/wac-graph/tests/graphs/implements-imports-resource/encoded.wat b/crates/wac-graph/tests/graphs/implements-imports-resource/encoded.wat new file mode 100644 index 00000000..19d04a64 --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-imports-resource/encoded.wat @@ -0,0 +1,56 @@ +(component + (type (;0;) + (instance + (export (;0;) "r" (type (sub resource))) + (export (;1;) "r2" (type (eq 0))) + (type (;2;) (own 0)) + (type (;3;) (borrow 1)) + (type (;4;) (func (param "a" 2) (param "b" 3))) + (export (;0;) "f" (func (type 4))) + ) + ) + (import "left" (implements "test:test/res") (instance (;0;) (type 0))) + (type (;1;) + (instance + (export (;0;) "r" (type (sub resource))) + (export (;1;) "r2" (type (eq 0))) + (type (;2;) (own 0)) + (type (;3;) (borrow 1)) + (type (;4;) (func (param "a" 2) (param "b" 3))) + (export (;0;) "f" (func (type 4))) + ) + ) + (import "right" (implements "test:test/res") (instance (;1;) (type 1))) + (type (;2;) + (component + (type (;0;) + (instance + (export (;0;) "r" (type (sub resource))) + (export (;1;) "r2" (type (eq 0))) + (type (;2;) (own 0)) + (type (;3;) (borrow 1)) + (type (;4;) (func (param "a" 2) (param "b" 3))) + (export (;0;) "f" (func (type 4))) + ) + ) + (import "left" (implements "test:test/res") (instance (;0;) (type 0))) + (type (;1;) + (instance + (export (;0;) "r" (type (sub resource))) + (export (;1;) "r2" (type (eq 0))) + (type (;2;) (own 0)) + (type (;3;) (borrow 1)) + (type (;4;) (func (param "a" 2) (param "b" 3))) + (export (;0;) "f" (func (type 4))) + ) + ) + (import "right" (implements "test:test/res") (instance (;1;) (type 1))) + ) + ) + (import "unlocked-dep=" (component (;0;) (type 2))) + (instance (;2;) (instantiate 0 + (with "left" (instance 0)) + (with "right" (instance 1)) + ) + ) +) diff --git a/crates/wac-graph/tests/graphs/implements-imports-resource/graph.json b/crates/wac-graph/tests/graphs/implements-imports-resource/graph.json new file mode 100644 index 00000000..cc244eb5 --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-imports-resource/graph.json @@ -0,0 +1,8 @@ +{ + "packages": [ + { "name": "test:consumer", "path": "consumer.wat" } + ], + "nodes": [ + { "type": "instantiation", "package": 0 } + ] +} diff --git a/crates/wac-graph/tests/graphs/implements-imports/encoded.wat b/crates/wac-graph/tests/graphs/implements-imports/encoded.wat index 592b1419..899010b3 100644 --- a/crates/wac-graph/tests/graphs/implements-imports/encoded.wat +++ b/crates/wac-graph/tests/graphs/implements-imports/encoded.wat @@ -6,8 +6,14 @@ ) ) (import "primary" (implements "test:test/iface") (instance (;0;) (type 0))) - (import "backup" (implements "test:test/iface") (instance (;1;) (type 0))) (type (;1;) + (instance + (type (;0;) (func)) + (export (;0;) "do-something" (func (type 0))) + ) + ) + (import "backup" (implements "test:test/iface") (instance (;1;) (type 1))) + (type (;2;) (component (type (;0;) (instance @@ -19,7 +25,7 @@ (import "backup" (implements "test:test/iface") (instance (;1;) (type 0))) ) ) - (import "unlocked-dep=" (component (;0;) (type 1))) + (import "unlocked-dep=" (component (;0;) (type 2))) (instance (;2;) (instantiate 0 (with "primary" (instance 0)) (with "backup" (instance 1)) diff --git a/crates/wac-graph/tests/graphs/implements-label-conflict/consumer-a.wat b/crates/wac-graph/tests/graphs/implements-label-conflict/consumer-a.wat new file mode 100644 index 00000000..58e865ba --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-label-conflict/consumer-a.wat @@ -0,0 +1,6 @@ +(component + (type (instance + (type (func (param "message" string))) + (export "log" (func (type 0))))) + (import "primary" (implements "test:test/logger@0.2.0") (instance (type 0))) +) diff --git a/crates/wac-graph/tests/graphs/implements-label-conflict/consumer-b.wat b/crates/wac-graph/tests/graphs/implements-label-conflict/consumer-b.wat new file mode 100644 index 00000000..34148a5e --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-label-conflict/consumer-b.wat @@ -0,0 +1,6 @@ +(component + (type (instance + (type (func (param "message" string))) + (export "log" (func (type 0))))) + (import "primary" (implements "test:test/logger@0.3.0") (instance (type 0))) +) diff --git a/crates/wac-graph/tests/graphs/implements-label-conflict/error.txt b/crates/wac-graph/tests/graphs/implements-label-conflict/error.txt new file mode 100644 index 00000000..a9641b77 --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-label-conflict/error.txt @@ -0,0 +1,5 @@ +failed to encode the graph + +Caused by: + 0: failed to merge the type definition for implicit import `primary` due to conflicting types + 1: interface `test:test/logger@0.3.0` cannot be merged with interface `test:test/logger@0.2.0` diff --git a/crates/wac-graph/tests/graphs/implements-label-conflict/graph.json b/crates/wac-graph/tests/graphs/implements-label-conflict/graph.json new file mode 100644 index 00000000..f9b3bd7f --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-label-conflict/graph.json @@ -0,0 +1,10 @@ +{ + "packages": [ + { "name": "test:consumer-a", "path": "consumer-a.wat" }, + { "name": "test:consumer-b", "path": "consumer-b.wat" } + ], + "nodes": [ + { "type": "instantiation", "package": 0 }, + { "type": "instantiation", "package": 1 } + ] +} diff --git a/crates/wac-graph/tests/graphs/implements-label-semver-merge/consumer-a.wat b/crates/wac-graph/tests/graphs/implements-label-semver-merge/consumer-a.wat new file mode 100644 index 00000000..58e865ba --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-label-semver-merge/consumer-a.wat @@ -0,0 +1,6 @@ +(component + (type (instance + (type (func (param "message" string))) + (export "log" (func (type 0))))) + (import "primary" (implements "test:test/logger@0.2.0") (instance (type 0))) +) diff --git a/crates/wac-graph/tests/graphs/implements-label-semver-merge/consumer-b.wat b/crates/wac-graph/tests/graphs/implements-label-semver-merge/consumer-b.wat new file mode 100644 index 00000000..8a6e57cc --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-label-semver-merge/consumer-b.wat @@ -0,0 +1,8 @@ +(component + (type (instance + (type (func (param "message" string))) + (export "log" (func (type 0))) + (type (func)) + (export "flush" (func (type 1))))) + (import "primary" (implements "test:test/logger@0.2.1") (instance (type 0))) +) diff --git a/crates/wac-graph/tests/graphs/implements-label-semver-merge/encoded.wat b/crates/wac-graph/tests/graphs/implements-label-semver-merge/encoded.wat new file mode 100644 index 00000000..18f2d7c7 --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-label-semver-merge/encoded.wat @@ -0,0 +1,45 @@ +(component + (type (;0;) + (instance + (type (;0;) (func (param "message" string))) + (export (;0;) "log" (func (type 0))) + (type (;1;) (func)) + (export (;1;) "flush" (func (type 1))) + ) + ) + (import "primary" (implements "test:test/logger@0.2.1") (instance (;0;) (type 0))) + (type (;1;) + (component + (type (;0;) + (instance + (type (;0;) (func (param "message" string))) + (export (;0;) "log" (func (type 0))) + ) + ) + (import "primary" (implements "test:test/logger@0.2.0") (instance (;0;) (type 0))) + ) + ) + (import "unlocked-dep=" (component (;0;) (type 1))) + (instance (;1;) (instantiate 0 + (with "primary" (instance 0)) + ) + ) + (type (;2;) + (component + (type (;0;) + (instance + (type (;0;) (func (param "message" string))) + (export (;0;) "log" (func (type 0))) + (type (;1;) (func)) + (export (;1;) "flush" (func (type 1))) + ) + ) + (import "primary" (implements "test:test/logger@0.2.1") (instance (;0;) (type 0))) + ) + ) + (import "unlocked-dep=" (component (;1;) (type 2))) + (instance (;2;) (instantiate 1 + (with "primary" (instance 0)) + ) + ) +) diff --git a/crates/wac-graph/tests/graphs/implements-label-semver-merge/graph.json b/crates/wac-graph/tests/graphs/implements-label-semver-merge/graph.json new file mode 100644 index 00000000..f9b3bd7f --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-label-semver-merge/graph.json @@ -0,0 +1,10 @@ +{ + "packages": [ + { "name": "test:consumer-a", "path": "consumer-a.wat" }, + { "name": "test:consumer-b", "path": "consumer-b.wat" } + ], + "nodes": [ + { "type": "instantiation", "package": 0 }, + { "type": "instantiation", "package": 1 } + ] +} diff --git a/crates/wac-graph/tests/graphs/implements-labels-unmerged/consumer-a.wat b/crates/wac-graph/tests/graphs/implements-labels-unmerged/consumer-a.wat new file mode 100644 index 00000000..58e865ba --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-labels-unmerged/consumer-a.wat @@ -0,0 +1,6 @@ +(component + (type (instance + (type (func (param "message" string))) + (export "log" (func (type 0))))) + (import "primary" (implements "test:test/logger@0.2.0") (instance (type 0))) +) diff --git a/crates/wac-graph/tests/graphs/implements-labels-unmerged/consumer-b.wat b/crates/wac-graph/tests/graphs/implements-labels-unmerged/consumer-b.wat new file mode 100644 index 00000000..afaf1a2d --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-labels-unmerged/consumer-b.wat @@ -0,0 +1,8 @@ +(component + (type (instance + (type (func (param "message" string))) + (export "log" (func (type 0))) + (type (func)) + (export "flush" (func (type 1))))) + (import "backup" (implements "test:test/logger@0.2.1") (instance (type 0))) +) diff --git a/crates/wac-graph/tests/graphs/implements-labels-unmerged/encoded.wat b/crates/wac-graph/tests/graphs/implements-labels-unmerged/encoded.wat new file mode 100644 index 00000000..102fabf2 --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-labels-unmerged/encoded.wat @@ -0,0 +1,52 @@ +(component + (type (;0;) + (instance + (type (;0;) (func (param "message" string))) + (export (;0;) "log" (func (type 0))) + ) + ) + (import "primary" (implements "test:test/logger@0.2.0") (instance (;0;) (type 0))) + (type (;1;) + (instance + (type (;0;) (func (param "message" string))) + (export (;0;) "log" (func (type 0))) + (type (;1;) (func)) + (export (;1;) "flush" (func (type 1))) + ) + ) + (import "backup" (implements "test:test/logger@0.2.1") (instance (;1;) (type 1))) + (type (;2;) + (component + (type (;0;) + (instance + (type (;0;) (func (param "message" string))) + (export (;0;) "log" (func (type 0))) + ) + ) + (import "primary" (implements "test:test/logger@0.2.0") (instance (;0;) (type 0))) + ) + ) + (import "unlocked-dep=" (component (;0;) (type 2))) + (instance (;2;) (instantiate 0 + (with "primary" (instance 0)) + ) + ) + (type (;3;) + (component + (type (;0;) + (instance + (type (;0;) (func (param "message" string))) + (export (;0;) "log" (func (type 0))) + (type (;1;) (func)) + (export (;1;) "flush" (func (type 1))) + ) + ) + (import "backup" (implements "test:test/logger@0.2.1") (instance (;0;) (type 0))) + ) + ) + (import "unlocked-dep=" (component (;1;) (type 3))) + (instance (;3;) (instantiate 1 + (with "backup" (instance 1)) + ) + ) +) diff --git a/crates/wac-graph/tests/graphs/implements-labels-unmerged/graph.json b/crates/wac-graph/tests/graphs/implements-labels-unmerged/graph.json new file mode 100644 index 00000000..f9b3bd7f --- /dev/null +++ b/crates/wac-graph/tests/graphs/implements-labels-unmerged/graph.json @@ -0,0 +1,10 @@ +{ + "packages": [ + { "name": "test:consumer-a", "path": "consumer-a.wat" }, + { "name": "test:consumer-b", "path": "consumer-b.wat" } + ], + "nodes": [ + { "type": "instantiation", "package": 0 }, + { "type": "instantiation", "package": 1 } + ] +} diff --git a/crates/wac-graph/tests/graphs/implements-satisfied/encoded.wat b/crates/wac-graph/tests/graphs/implements-satisfied/encoded.wat index e5a9fa7c..e6d89d30 100644 --- a/crates/wac-graph/tests/graphs/implements-satisfied/encoded.wat +++ b/crates/wac-graph/tests/graphs/implements-satisfied/encoded.wat @@ -47,4 +47,5 @@ (with "primary" (instance 3)) ) ) + (export (;6;) "renamed" (instance 3)) ) diff --git a/crates/wac-graph/tests/graphs/implements-satisfied/graph.json b/crates/wac-graph/tests/graphs/implements-satisfied/graph.json index 6fd74434..01fdf5c9 100644 --- a/crates/wac-graph/tests/graphs/implements-satisfied/graph.json +++ b/crates/wac-graph/tests/graphs/implements-satisfied/graph.json @@ -13,5 +13,8 @@ "arguments": [ { "source": 2, "target": 4, "name": "primary" }, { "source": 3, "target": 4, "name": "backup" } + ], + "exports": [ + { "node": 2, "name": "renamed" } ] } diff --git a/crates/wac-graph/tests/graphs/merged-usings/encoded.wat b/crates/wac-graph/tests/graphs/merged-usings/encoded.wat index 4333754f..dbb2c45c 100644 --- a/crates/wac-graph/tests/graphs/merged-usings/encoded.wat +++ b/crates/wac-graph/tests/graphs/merged-usings/encoded.wat @@ -35,17 +35,16 @@ ) ) (import "foo:bar/qux" (instance (;1;) (type 2))) - (alias export 0 "x" (type (;3;))) - (type (;4;) + (type (;3;) (instance - (alias outer 1 3 (type (;0;))) + (alias outer 1 1 (type (;0;))) (export (;1;) "x" (type (eq 0))) (type (;2;) (own 1)) (type (;3;) (func (result 2))) (export (;0;) "f" (func (type 3))) ) ) - (export (;2;) "foo:bar/qux" (instance (type 4))) + (export (;2;) "foo:bar/qux" (instance (type 3))) ) ) (import "unlocked-dep=" (component (;0;) (type 3))) @@ -74,17 +73,16 @@ ) ) (import "foo:bar/qux" (instance (;1;) (type 2))) - (alias export 0 "x" (type (;3;))) - (type (;4;) + (type (;3;) (instance - (alias outer 1 3 (type (;0;))) + (alias outer 1 1 (type (;0;))) (export (;1;) "x" (type (eq 0))) (type (;2;) (own 1)) (type (;3;) (func (result 2))) (export (;0;) "f" (func (type 3))) ) ) - (export (;2;) "foo:bar/qux" (instance (type 4))) + (export (;2;) "foo:bar/qux" (instance (type 3))) ) ) (import "unlocked-dep=" (component (;1;) (type 4))) diff --git a/crates/wac-graph/tests/graphs/nested-use-type/encoded.wat b/crates/wac-graph/tests/graphs/nested-use-type/encoded.wat index ee41a7a6..a6a1fe80 100644 --- a/crates/wac-graph/tests/graphs/nested-use-type/encoded.wat +++ b/crates/wac-graph/tests/graphs/nested-use-type/encoded.wat @@ -41,16 +41,15 @@ (import "region" (type (;4;) (eq 3))) (type (;5;) (func (result 4))) (export (;0;) "make" (func (type 5))) - (alias export 0 "point" (type (;6;))) - (type (;7;) + (type (;6;) (instance - (alias outer 1 6 (type (;0;))) + (alias outer 1 1 (type (;0;))) (export (;1;) "point" (type (eq 0))) (type (;2;) (record (field "inner" 1))) (export (;3;) "region" (type (eq 2))) ) ) - (export (;2;) "test:prod/defs" (instance (type 7))) + (export (;2;) "test:prod/defs" (instance (type 6))) ) ) (import "unlocked-dep=" (component (;0;) (type 5))) diff --git a/crates/wac-types/src/aggregator.rs b/crates/wac-types/src/aggregator.rs index 2cb61319..7c9b0882 100644 --- a/crates/wac-types/src/aggregator.rs +++ b/crates/wac-types/src/aggregator.rs @@ -1,5 +1,5 @@ use crate::{ - names::{alternate_lookup_key, are_semver_compatible}, + names::{alternate_lookup_key, are_semver_compatible, is_label}, DefinedType, DefinedTypeId, FuncType, FuncTypeId, Interface, InterfaceId, ItemKind, ModuleTypeId, Record, Resource, ResourceAlias, ResourceId, SubtypeChecker, Type, Types, UsedType, ValueType, Variant, World, WorldId, @@ -135,7 +135,7 @@ impl TypeAggregator { return Ok(self); } - let remapped = self.remap_item_kind(types, kind, checker)?; + let remapped = self.remap_extern(name, types, kind, checker)?; let prev = self.imports.insert(name.to_string(), remapped); assert!(prev.is_none()); Ok(self) @@ -184,20 +184,32 @@ impl TypeAggregator { id: InterfaceId, checker: &mut SubtypeChecker, ) -> Result<()> { + self.merge_interface_names(existing, types, id)?; + // Merge the used types of the two interfaces self.merge_interface_used_types(existing, types, id, checker)?; // Merge the interface's exports for (name, source_kind) in &types[id].exports { + let mut merged_name = None; if let Some(target_kind) = self.types[existing].exports.get(name).copied() { + if let (true, ItemKind::Instance(target), ItemKind::Instance(source)) = + (is_label(name), target_kind, *source_kind) + { + self.merge_interface_names(target, types, source)?; + merged_name = Some(self.types[target].id.clone()); + } // If the source kind is already a subtype of the target, do nothing if checker .is_subtype(*source_kind, types, target_kind, &self.types) .is_ok() { - // Keep track that the source type should be replaced with the - // target type wherever it's used. - self.remapped.insert(source_kind.ty(), target_kind.ty()); + // Labelled instances have a separate definition per extern. + // Caching their mapping would make a later named import + // reuse this label's potentially narrower definition. + if !(is_label(name) && matches!(source_kind, ItemKind::Instance(_))) { + self.remapped.insert(source_kind.ty(), target_kind.ty()); + } continue; } @@ -208,13 +220,57 @@ impl TypeAggregator { .with_context(|| format!("mismatched type for export `{name}`"))?; } - let remapped = self.remap_item_kind(types, *source_kind, checker)?; + let remapped = self.remap_extern(name, types, *source_kind, checker)?; + if let (Some(name), ItemKind::Instance(id)) = (merged_name, remapped) { + self.types[id].id = name; + } self.types[existing].exports.insert(name.clone(), remapped); } Ok(()) } + /// Checks that two named interfaces being merged are the same interface on + /// compatible semver tracks, and names the merged interface after the + /// higher version. + /// + /// The merged interface can have exports only the higher version defines, + /// and an `(implements "I")` import of it encodes its name as `I`. + /// + /// Merging a named interface with an unnamed one (e.g. a label imported + /// both as `(implements "I")` and as an inline interface) keeps the name, + /// whichever is merged first. + fn merge_interface_names( + &mut self, + existing: InterfaceId, + types: &Types, + id: InterfaceId, + ) -> Result<()> { + let (existing_name, name) = match (&self.types[existing].id, &types[id].id) { + (Some(existing_name), Some(name)) => (existing_name, name), + (None, Some(name)) => { + self.types[existing].id = Some(name.clone()); + return Ok(()); + } + _ => return Ok(()), + }; + + if !are_semver_compatible(existing_name, name) { + bail!("interface `{name}` cannot be merged with interface `{existing_name}`"); + } + + if let (Some((_, existing_version)), Some((_, version))) = ( + alternate_lookup_key(existing_name), + alternate_lookup_key(name), + ) { + if version > existing_version { + self.types[existing].id = Some(name.clone()); + } + } + + Ok(()) + } + fn merge_interface_used_types( &mut self, existing: InterfaceId, @@ -284,7 +340,14 @@ impl TypeAggregator { // Merge the worlds's imports checker.invert(); for (name, source_kind) in &types[id].imports { + let mut merged_name = None; if let Some(target_kind) = self.types[existing].imports.get(name).copied() { + if let (true, ItemKind::Instance(target), ItemKind::Instance(source)) = + (is_label(name), target_kind, *source_kind) + { + self.merge_interface_names(target, types, source)?; + merged_name = Some(self.types[target].id.clone()); + } // If the target kind is already a subtype of the source, do nothing if checker .is_subtype(target_kind, &self.types, *source_kind, types) @@ -300,7 +363,10 @@ impl TypeAggregator { .with_context(|| format!("mismatched type for import `{name}`"))?; } - let remapped = self.remap_item_kind(types, *source_kind, checker)?; + let remapped = self.remap_extern(name, types, *source_kind, checker)?; + if let (Some(name), ItemKind::Instance(id)) = (merged_name, remapped) { + self.types[id].id = name; + } self.types[existing].imports.insert(name.clone(), remapped); } @@ -308,7 +374,14 @@ impl TypeAggregator { // Merge the worlds's exports for (name, source_kind) in &types[id].exports { + let mut merged_name = None; if let Some(target_kind) = self.types[existing].exports.get(name).copied() { + if let (true, ItemKind::Instance(target), ItemKind::Instance(source)) = + (is_label(name), target_kind, *source_kind) + { + self.merge_interface_names(target, types, source)?; + merged_name = Some(self.types[target].id.clone()); + } // If the source kind is already a subtype of the target, do nothing if checker .is_subtype(*source_kind, types, target_kind, &self.types) @@ -324,7 +397,10 @@ impl TypeAggregator { .with_context(|| format!("mismatched type for export `{name}`"))?; } - let remapped = self.remap_item_kind(types, *source_kind, checker)?; + let remapped = self.remap_extern(name, types, *source_kind, checker)?; + if let (Some(name), ItemKind::Instance(id)) = (merged_name, remapped) { + self.types[id].id = name; + } self.types[existing].exports.insert(name.clone(), remapped); } @@ -564,6 +640,26 @@ impl TypeAggregator { Ok(()) } + /// Remaps the item kind of an import or export named `name`. + /// + /// Each label (e.g. of an `(implements "I")` import) is an extern of its + /// own, so a labelled instance isn't merged with other externs of its + /// interface: its type only requires what that label's users need. + fn remap_extern( + &mut self, + name: &str, + types: &Types, + kind: ItemKind, + checker: &mut SubtypeChecker, + ) -> Result { + match kind { + ItemKind::Instance(id) if is_label(name) => Ok(ItemKind::Instance( + self.remap_labelled_interface(types, id, checker)?, + )), + _ => self.remap_item_kind(types, kind, checker), + } + } + fn remap_item_kind( &mut self, types: &Types, @@ -738,8 +834,46 @@ impl TypeAggregator { }; } + let interface = self.remap_interface_definition(types, id, checker)?; + let remapped = self.types.add_interface(interface); + let prev = self + .remapped + .insert(Type::Interface(id), Type::Interface(remapped)); + assert!(prev.is_none()); + + if let Some(name) = self.types[remapped].id.as_ref() { + let prev = self.interfaces.insert(name.clone(), remapped); + assert!(prev.is_none()); + } + + Ok(remapped) + } + + /// Remaps the interface of a labelled instance extern to a new interface. + /// + /// Unlike [`Self::remap_interface`], the new interface neither merges with + /// other remappings of the interface nor is found by its name, as each + /// label is an extern of its own. The types it uses or exports are + /// remapped as usual, so they may be shared with other interfaces. + fn remap_labelled_interface( + &mut self, + types: &Types, + id: InterfaceId, + checker: &mut SubtypeChecker, + ) -> Result { + let interface = self.remap_interface_definition(types, id, checker)?; + Ok(self.types.add_interface(interface)) + } + + /// Remaps the used types and exports of an interface. + fn remap_interface_definition( + &mut self, + types: &Types, + id: InterfaceId, + checker: &mut SubtypeChecker, + ) -> Result { let ty = &types[id]; - let interface = Interface { + Ok(Interface { id: ty.id.clone(), uses: ty .uses @@ -761,22 +895,9 @@ impl TypeAggregator { exports: ty .exports .iter() - .map(|(n, k)| Ok((n.clone(), self.remap_item_kind(types, *k, checker)?))) + .map(|(n, k)| Ok((n.clone(), self.remap_extern(n, types, *k, checker)?))) .collect::>()?, - }; - - let remapped = self.types.add_interface(interface); - let prev = self - .remapped - .insert(Type::Interface(id), Type::Interface(remapped)); - assert!(prev.is_none()); - - if let Some(name) = self.types[remapped].id.as_ref() { - let prev = self.interfaces.insert(name.clone(), remapped); - assert!(prev.is_none()); - } - - Ok(remapped) + }) } fn remap_world( @@ -815,12 +936,12 @@ impl TypeAggregator { imports: ty .imports .iter() - .map(|(n, k)| Ok((n.clone(), self.remap_item_kind(types, *k, checker)?))) + .map(|(n, k)| Ok((n.clone(), self.remap_extern(n, types, *k, checker)?))) .collect::>()?, exports: ty .exports .iter() - .map(|(n, k)| Ok((n.clone(), self.remap_item_kind(types, *k, checker)?))) + .map(|(n, k)| Ok((n.clone(), self.remap_extern(n, types, *k, checker)?))) .collect::>()?, }; @@ -1470,4 +1591,398 @@ mod tests { ) .unwrap(); } + + // Helper to create a logger interface named `name` exporting a no-op func + // for each of `funcs` + fn make_logger(types: &mut Types, name: &str, funcs: &[&str]) -> InterfaceId { + let exports = funcs + .iter() + .map(|f| (*f, ItemKind::Func(make_func_type(types)))) + .collect(); + make_interface(types, Some(name), exports) + } + + #[test] + fn aggregate_labels_of_one_interface_stay_separate() { + let mut types1 = Types::default(); + let primary = make_logger(&mut types1, "a:b/logger@0.2.0", &["log"]); + let mut types2 = Types::default(); + let backup = make_logger(&mut types2, "a:b/logger@0.2.1", &["log", "flush"]); + + let mut cache = HashSet::new(); + let mut checker = SubtypeChecker::new(&mut cache); + + let agg = TypeAggregator::new() + .aggregate( + "primary", + &types1, + ItemKind::Instance(primary), + &mut checker, + ) + .unwrap() + .aggregate("backup", &types2, ItemKind::Instance(backup), &mut checker) + .unwrap(); + + // Each label keeps its own interface, version, and exports + let imports: Vec<_> = agg + .imports() + .map(|(name, kind)| match kind { + ItemKind::Instance(id) => { + let interface = &agg.types()[id]; + let exports: Vec<_> = interface.exports.keys().map(String::as_str).collect(); + (name, interface.id.as_deref().unwrap(), exports) + } + _ => panic!("expected an instance"), + }) + .collect(); + assert_eq!( + imports, + [ + ("primary", "a:b/logger@0.2.0", vec!["log"]), + ("backup", "a:b/logger@0.2.1", vec!["log", "flush"]), + ] + ); + } + + #[test] + fn aggregate_label_keeps_highest_version() { + let mut types1 = Types::default(); + let lower = make_logger(&mut types1, "a:b/logger@0.2.0", &["log"]); + let mut types2 = Types::default(); + let higher = make_logger(&mut types2, "a:b/logger@0.2.1", &["log", "flush"]); + + let mut cache = HashSet::new(); + let mut checker = SubtypeChecker::new(&mut cache); + + let agg = TypeAggregator::new() + .aggregate("primary", &types1, ItemKind::Instance(lower), &mut checker) + .unwrap() + .aggregate("primary", &types2, ItemKind::Instance(higher), &mut checker) + .unwrap(); + + // The label's merged interface is named after the version that + // defines all of its exports + let items: Vec<_> = agg.imports().collect(); + assert_eq!(items.len(), 1); + let ItemKind::Instance(id) = items[0].1 else { + panic!("expected an instance"); + }; + let interface = &agg.types()[id]; + assert_eq!(interface.id.as_deref(), Some("a:b/logger@0.2.1")); + assert_eq!( + interface.exports.keys().collect::>(), + ["log", "flush"] + ); + } + + #[test] + fn aggregate_label_of_incompatible_interfaces_fails() { + let mut types1 = Types::default(); + let lower = make_logger(&mut types1, "a:b/logger@0.2.0", &["log"]); + let mut types2 = Types::default(); + let higher = make_logger(&mut types2, "a:b/logger@0.3.0", &["log"]); + + let mut cache = HashSet::new(); + let mut checker = SubtypeChecker::new(&mut cache); + + let result = TypeAggregator::new() + .aggregate("primary", &types1, ItemKind::Instance(lower), &mut checker) + .unwrap() + .aggregate("primary", &types2, ItemKind::Instance(higher), &mut checker); + let err = result.unwrap_err().to_string(); + assert_eq!( + err, + "interface `a:b/logger@0.3.0` cannot be merged with interface `a:b/logger@0.2.0`" + ); + } + + // Helper to get the interface name and export names of an instance + fn describe(agg: &TypeAggregator, kind: ItemKind) -> (Option<&str>, Vec<&str>) { + let ItemKind::Instance(id) = kind else { + panic!("expected an instance"); + }; + let interface = &agg.types()[id]; + ( + interface.id.as_deref(), + interface.exports.keys().map(String::as_str).collect(), + ) + } + + #[test] + fn aggregate_label_keeps_interface_name() { + let mut types = Types::default(); + let named = make_logger(&mut types, "a:b/logger", &["log"]); + let func = make_func_type(&mut types); + let unnamed = make_interface(&mut types, None, vec![("log", ItemKind::Func(func))]); + + // Whichever is aggregated first, the label implements the named interface + for order in [[unnamed, named], [named, unnamed]] { + let mut cache = HashSet::new(); + let mut checker = SubtypeChecker::new(&mut cache); + + let agg = TypeAggregator::new() + .aggregate( + "primary", + &types, + ItemKind::Instance(order[0]), + &mut checker, + ) + .unwrap() + .aggregate( + "primary", + &types, + ItemKind::Instance(order[1]), + &mut checker, + ) + .unwrap(); + + let items: Vec<_> = agg.imports().collect(); + assert_eq!(items.len(), 1); + assert_eq!( + describe(&agg, items[0].1), + (Some("a:b/logger"), vec!["log"]) + ); + } + } + + #[test] + fn aggregate_label_in_component_import_stays_separate() { + let mut types = Types::default(); + let label = make_logger(&mut types, "a:b/logger", &["log"]); + let logger = make_logger(&mut types, "a:b/logger", &["log", "flush"]); + let world = types.add_world(World { + id: None, + uses: IndexMap::new(), + imports: [("primary".to_string(), ItemKind::Instance(label))] + .into_iter() + .collect(), + exports: IndexMap::new(), + }); + + let mut cache = HashSet::new(); + let mut checker = SubtypeChecker::new(&mut cache); + + let agg = TypeAggregator::new() + .aggregate("comp", &types, ItemKind::Component(world), &mut checker) + .unwrap() + .aggregate( + "a:b/logger", + &types, + ItemKind::Instance(logger), + &mut checker, + ) + .unwrap(); + + // The label within the component type only requires its own exports + let imports: IndexMap<_, _> = agg.imports().collect(); + let ItemKind::Component(world) = imports["comp"] else { + panic!("expected a component"); + }; + let label = agg.types()[world].imports["primary"]; + assert_eq!(describe(&agg, label), (Some("a:b/logger"), vec!["log"])); + assert_eq!( + describe(&agg, imports["a:b/logger"]), + (Some("a:b/logger"), vec!["log", "flush"]) + ); + } + + #[test] + fn aggregate_component_labels_check_names_even_when_shapes_match() { + let make_world = |version: &str| { + let mut types = Types::default(); + let logger = make_logger(&mut types, version, &["log"]); + let world = types.add_world(World { + id: None, + uses: IndexMap::new(), + imports: [("primary".to_string(), ItemKind::Instance(logger))] + .into_iter() + .collect(), + exports: [("backup".to_string(), ItemKind::Instance(logger))] + .into_iter() + .collect(), + }); + (types, world) + }; + + let (lower_types, lower_world) = make_world("a:b/logger@0.2.0"); + let (higher_types, higher_world) = make_world("a:b/logger@0.2.1"); + for (first_types, first_world, second_types, second_world) in [ + (&lower_types, lower_world, &higher_types, higher_world), + (&higher_types, higher_world, &lower_types, lower_world), + ] { + let mut cache = HashSet::new(); + let mut checker = SubtypeChecker::new(&mut cache); + let agg = TypeAggregator::new() + .aggregate( + "comp", + first_types, + ItemKind::Component(first_world), + &mut checker, + ) + .unwrap() + .aggregate( + "comp", + second_types, + ItemKind::Component(second_world), + &mut checker, + ) + .unwrap(); + let ItemKind::Component(world) = agg.imports().next().unwrap().1 else { + panic!("expected a component"); + }; + for label in [ + agg.types()[world].imports["primary"], + agg.types()[world].exports["backup"], + ] { + assert_eq!(describe(&agg, label).0, Some("a:b/logger@0.2.1")); + } + } + + let (incompatible_types, incompatible_world) = make_world("a:b/logger@0.3.0"); + let mut cache = HashSet::new(); + let mut checker = SubtypeChecker::new(&mut cache); + let result = TypeAggregator::new() + .aggregate( + "comp", + &lower_types, + ItemKind::Component(lower_world), + &mut checker, + ) + .unwrap() + .aggregate( + "comp", + &incompatible_types, + ItemKind::Component(incompatible_world), + &mut checker, + ); + assert!(result.is_err()); + } + + #[test] + fn aggregate_nested_labelled_exports_stay_separate() { + let mut types = Types::default(); + let primary = make_logger(&mut types, "a:b/logger@0.2.0", &["log"]); + let backup = make_logger(&mut types, "a:b/logger@0.2.1", &["log", "flush"]); + let outer = make_interface( + &mut types, + Some("a:b/outer"), + vec![ + ("primary", ItemKind::Instance(primary)), + ("backup", ItemKind::Instance(backup)), + ], + ); + let mut cache = HashSet::new(); + let mut checker = SubtypeChecker::new(&mut cache); + let agg = TypeAggregator::new() + .aggregate("a:b/outer", &types, ItemKind::Instance(outer), &mut checker) + .unwrap(); + let ItemKind::Instance(outer) = agg.imports().next().unwrap().1 else { + panic!("expected an instance"); + }; + let exports = &agg.types()[outer].exports; + assert_eq!( + describe(&agg, exports["primary"]), + (Some("a:b/logger@0.2.0"), vec!["log"]) + ); + assert_eq!( + describe(&agg, exports["backup"]), + (Some("a:b/logger@0.2.1"), vec!["log", "flush"]) + ); + } + + #[test] + fn aggregate_nested_labelled_export_keeps_highest_version() { + let make_outer = |version: &str| { + let mut types = Types::default(); + let logger = make_logger(&mut types, version, &["log"]); + let outer = make_interface( + &mut types, + Some("a:b/outer"), + vec![("primary", ItemKind::Instance(logger))], + ); + (types, outer) + }; + let (lower_types, lower) = make_outer("a:b/logger@0.2.0"); + let (higher_types, higher) = make_outer("a:b/logger@0.2.1"); + let mut cache = HashSet::new(); + let mut checker = SubtypeChecker::new(&mut cache); + let agg = TypeAggregator::new() + .aggregate( + "a:b/outer", + &lower_types, + ItemKind::Instance(lower), + &mut checker, + ) + .unwrap() + .aggregate( + "a:b/outer", + &higher_types, + ItemKind::Instance(higher), + &mut checker, + ) + .unwrap(); + let ItemKind::Instance(outer) = agg.imports().next().unwrap().1 else { + panic!("expected an instance"); + }; + assert_eq!( + describe(&agg, agg.types()[outer].exports["primary"]).0, + Some("a:b/logger@0.2.1") + ); + } + + #[test] + fn aggregate_named_import_after_merging_nested_labels() { + let mut types = Types::default(); + let lower = make_logger(&mut types, "a:b/logger@0.2.0", &["log"]); + let higher = make_logger(&mut types, "a:b/logger@0.2.1", &["log", "flush"]); + let outer_lower = make_interface( + &mut types, + None, + vec![("primary", ItemKind::Instance(lower))], + ); + let outer_higher = make_interface( + &mut types, + None, + vec![("primary", ItemKind::Instance(higher))], + ); + let mut cache = HashSet::new(); + let mut checker = SubtypeChecker::new(&mut cache); + let agg = TypeAggregator::new() + .aggregate( + "outer", + &types, + ItemKind::Instance(outer_lower), + &mut checker, + ) + .unwrap() + .aggregate( + "outer", + &types, + ItemKind::Instance(outer_higher), + &mut checker, + ) + .unwrap() + .aggregate( + "a:b/logger@0.2.1", + &types, + ItemKind::Instance(higher), + &mut checker, + ) + .unwrap(); + + let imports: IndexMap<_, _> = agg.imports().collect(); + // The named import must retain its full definition even though the + // same source interface was merged into a narrower labelled export. + assert_eq!( + describe(&agg, imports["a:b/logger@0.2.1"]), + (Some("a:b/logger@0.2.1"), vec!["log", "flush"]) + ); + let ItemKind::Instance(outer) = imports["outer"] else { + panic!("expected an instance"); + }; + assert_eq!( + describe(&agg, agg.types()[outer].exports["primary"]).1, + vec!["log"] + ); + } } diff --git a/crates/wac-types/src/names.rs b/crates/wac-types/src/names.rs index 34874c9c..bf0cd7d7 100644 --- a/crates/wac-types/src/names.rs +++ b/crates/wac-types/src/names.rs @@ -4,6 +4,7 @@ use anyhow::{bail, Result}; use core::hash::Hash; use indexmap::IndexMap; use semver::Version; +use wasmparser::names::{ComponentName, ComponentNameKind}; /// A semver-aware map for imports/exports of a component. /// @@ -213,6 +214,16 @@ pub fn are_semver_compatible(a: &str, b: &str) -> bool { } } +/// Returns true if an import or export name is a plain-name label, such as +/// the `primary` of `(import "primary" (implements "a:b/c") ...)`, rather +/// than an interface name or another kind of name. +pub fn is_label(name: &str) -> bool { + matches!( + ComponentName::new(name, 0).as_ref().map(|n| n.kind()), + Ok(ComponentNameKind::Label(_)) + ) +} + pub(crate) fn alternate_lookup_key(name: &str) -> Option<(&str, Version)> { let at = name.find('@')?; let version_string = &name[at + 1..]; diff --git a/crates/wac-types/src/package.rs b/crates/wac-types/src/package.rs index 0ebdb94d..dde48ab9 100644 --- a/crates/wac-types/src/package.rs +++ b/crates/wac-types/src/package.rs @@ -507,11 +507,25 @@ impl<'a> TypeConverter<'a> { name: Option<&str>, id: wasm::ComponentInstanceTypeId, ) -> Result { + let name = name.filter(|n| n.contains(':')); let key = wasm::AnyTypeId::Component(wasm::ComponentAnyTypeId::Instance(id)); if let Some(ty) = self.cache.get(&key) { match ty { Entity::Type(Type::Interface(id)) => { - return Ok(*id); + let interface = &self.types[*id]; + if interface.id.as_deref() == name { + return Ok(*id); + } + + // An instance can be imported by its interface name and + // re-exported under a label, with or without `implements`. + // Share its type contents, but use this extern's metadata + // instead of the name cached for an earlier extern. + let interface = Interface { + id: name.map(ToOwned::to_owned), + ..interface.clone() + }; + return Ok(self.types.add_interface(interface)); } _ => panic!("invalid cached type"), } @@ -520,7 +534,7 @@ impl<'a> TypeConverter<'a> { let wasm_types = self.wasm_types.clone(); let instance_ty = &wasm_types[id]; let id = self.types.add_interface(Interface { - id: name.and_then(|n| n.contains(':').then(|| n.to_owned())), + id: name.map(ToOwned::to_owned), uses: Default::default(), exports: IndexMap::with_capacity(instance_ty.exports.len()), });