From f18cbc0d8406acf25e5372aa924c1e0264f0afd4 Mon Sep 17 00:00:00 2001 From: Salman Saghafi Date: Thu, 11 Jun 2026 17:50:43 -0400 Subject: [PATCH 1/2] fix: alias used types when encoding type references by id A type used from another interface, referenced only by id, was re-encoded as a fresh local definition instead of the alias `use_aliases` emitted, failing validation. Record the alias under the type's ids so id-keyed lookups reuse it. --- crates/wac-graph/src/encoding.rs | 22 +++- .../graphs/nested-use-type/cons/cons.wit | 8 ++ .../graphs/nested-use-type/cons/deps/prod.wit | 15 +++ .../tests/graphs/nested-use-type/encoded.wat | 102 ++++++++++++++++++ .../tests/graphs/nested-use-type/graph.json | 55 ++++++++++ .../tests/graphs/nested-use-type/prod.wit | 22 ++++ 6 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 crates/wac-graph/tests/graphs/nested-use-type/cons/cons.wit create mode 100644 crates/wac-graph/tests/graphs/nested-use-type/cons/deps/prod.wit create mode 100644 crates/wac-graph/tests/graphs/nested-use-type/encoded.wat create mode 100644 crates/wac-graph/tests/graphs/nested-use-type/graph.json create mode 100644 crates/wac-graph/tests/graphs/nested-use-type/prod.wit diff --git a/crates/wac-graph/src/encoding.rs b/crates/wac-graph/src/encoding.rs index 54aaba5..64a4f8e 100644 --- a/crates/wac-graph/src/encoding.rs +++ b/crates/wac-graph/src/encoding.rs @@ -264,7 +264,12 @@ impl<'a> TypeEncoder<'a> { index } - fn use_aliases(&self, state: &mut State, uses: &'a IndexMap) { + fn use_aliases( + &self, + state: &mut State, + uses: &'a IndexMap, + items: &'a IndexMap, + ) { state.current.type_aliases.clear(); for (name, used) in uses { @@ -286,6 +291,17 @@ impl<'a> TypeEncoder<'a> { ); state.current.type_aliases.insert(name.clone(), index); + + // Record the alias under both ids that denote this used type (the + // interface's export and the using entity's own item) so a later + // id-keyed reference reuses it instead of re-encoding a local copy: + // a type import must reference a named type, not a local definition. + if let ItemKind::Type(ty) = kind { + state.current.type_indexes.insert(*ty, index); + } + if let Some(ItemKind::Type(ty)) = items.get(name) { + state.current.type_indexes.insert(*ty, index); + } } } @@ -297,7 +313,7 @@ impl<'a> TypeEncoder<'a> { } // Encode any required aliases - self.use_aliases(state, &interface.uses); + self.use_aliases(state, &interface.uses, &interface.exports); state.push(Encodable::Instance(InstanceType::default())); // Otherwise, export all exports @@ -335,7 +351,7 @@ impl<'a> TypeEncoder<'a> { self.import_deps(state, used.interface); } - self.use_aliases(state, &world.uses); + self.use_aliases(state, &world.uses, &world.imports); for (name, kind) in &world.imports { self.import(state, name, *kind); diff --git a/crates/wac-graph/tests/graphs/nested-use-type/cons/cons.wit b/crates/wac-graph/tests/graphs/nested-use-type/cons/cons.wit new file mode 100644 index 0000000..83890a0 --- /dev/null +++ b/crates/wac-graph/tests/graphs/nested-use-type/cons/cons.wit @@ -0,0 +1,8 @@ +package test:cons; + +world cons { + import test:prod/defs; + use test:prod/defs.{region}; + import make: func() -> region; + export run: func() -> s32; +} diff --git a/crates/wac-graph/tests/graphs/nested-use-type/cons/deps/prod.wit b/crates/wac-graph/tests/graphs/nested-use-type/cons/deps/prod.wit new file mode 100644 index 0000000..e842afb --- /dev/null +++ b/crates/wac-graph/tests/graphs/nested-use-type/cons/deps/prod.wit @@ -0,0 +1,15 @@ +package test:prod; + +interface ext { + record point { + x: s32, + } +} + +interface defs { + use ext.{point}; + + record region { + inner: point, + } +} diff --git a/crates/wac-graph/tests/graphs/nested-use-type/encoded.wat b/crates/wac-graph/tests/graphs/nested-use-type/encoded.wat new file mode 100644 index 0000000..ee41a7a --- /dev/null +++ b/crates/wac-graph/tests/graphs/nested-use-type/encoded.wat @@ -0,0 +1,102 @@ +(component + (type (;0;) + (instance + (type (;0;) (record (field "x" s32))) + (export (;1;) "point" (type (eq 0))) + ) + ) + (import "test:prod/ext" (instance (;0;) (type 0))) + (alias export 0 "point" (type (;1;))) + (type (;2;) + (instance + (alias outer 1 1 (type (;0;))) + (export (;1;) "point" (type (eq 0))) + (type (;2;) (record (field "inner" 1))) + (export (;3;) "region" (type (eq 2))) + ) + ) + (import "test:prod/defs" (instance (;1;) (type 2))) + (type (;3;) (record (field "inner" 1))) + (import "region" (type (;4;) (eq 3))) + (type (;5;) + (component + (type (;0;) + (instance + (type (;0;) (record (field "x" s32))) + (export (;1;) "point" (type (eq 0))) + ) + ) + (import "test:prod/ext" (instance (;0;) (type 0))) + (alias export 0 "point" (type (;1;))) + (type (;2;) + (instance + (alias outer 1 1 (type (;0;))) + (export (;1;) "point" (type (eq 0))) + (type (;2;) (record (field "inner" 1))) + (export (;3;) "region" (type (eq 2))) + ) + ) + (import "test:prod/defs" (instance (;1;) (type 2))) + (type (;3;) (record (field "inner" 1))) + (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;) + (instance + (alias outer 1 6 (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))) + ) + ) + (import "unlocked-dep=" (component (;0;) (type 5))) + (instance (;2;) (instantiate 0 + (with "test:prod/ext" (instance 0)) + (with "test:prod/defs" (instance 1)) + (with "region" (type 4)) + ) + ) + (alias export 2 "test:prod/defs" (instance (;3;))) + (alias export 2 "make" (func (;0;))) + (type (;6;) + (component + (type (;0;) + (instance + (type (;0;) (record (field "x" s32))) + (export (;1;) "point" (type (eq 0))) + ) + ) + (import "test:prod/ext" (instance (;0;) (type 0))) + (alias export 0 "point" (type (;1;))) + (type (;2;) + (instance + (alias outer 1 1 (type (;0;))) + (export (;1;) "point" (type (eq 0))) + (type (;2;) (record (field "inner" 1))) + (export (;3;) "region" (type (eq 2))) + ) + ) + (import "test:prod/defs" (instance (;1;) (type 2))) + (type (;3;) (record (field "inner" 1))) + (import "region" (type (;4;) (eq 3))) + (type (;5;) (func (result 4))) + (import "make" (func (;0;) (type 5))) + (type (;6;) (func (result s32))) + (export (;1;) "run" (func (type 6))) + ) + ) + (import "unlocked-dep=" (component (;1;) (type 6))) + (instance (;4;) (instantiate 1 + (with "make" (func 0)) + (with "test:prod/defs" (instance 3)) + (with "test:prod/ext" (instance 0)) + (with "region" (type 4)) + ) + ) + (alias export 4 "run" (func (;1;))) + (export (;2;) "run" (func 1)) +) diff --git a/crates/wac-graph/tests/graphs/nested-use-type/graph.json b/crates/wac-graph/tests/graphs/nested-use-type/graph.json new file mode 100644 index 0000000..84f4898 --- /dev/null +++ b/crates/wac-graph/tests/graphs/nested-use-type/graph.json @@ -0,0 +1,55 @@ +{ + "packages": [ + { + "name": "test:prod", + "path": "prod.wit" + }, + { + "name": "test:cons", + "path": "cons" + } + ], + "nodes": [ + { + "type": "instantiation", + "package": 0 + }, + { + "type": "alias", + "source": 0, + "export": "test:prod/defs" + }, + { + "type": "alias", + "source": 0, + "export": "make" + }, + { + "type": "instantiation", + "package": 1 + }, + { + "type": "alias", + "source": 3, + "export": "run" + } + ], + "arguments": [ + { + "source": 1, + "target": 3, + "name": "test:prod/defs" + }, + { + "source": 2, + "target": 3, + "name": "make" + } + ], + "exports": [ + { + "node": 4, + "name": "run" + } + ] +} diff --git a/crates/wac-graph/tests/graphs/nested-use-type/prod.wit b/crates/wac-graph/tests/graphs/nested-use-type/prod.wit new file mode 100644 index 0000000..afdb5a9 --- /dev/null +++ b/crates/wac-graph/tests/graphs/nested-use-type/prod.wit @@ -0,0 +1,22 @@ +package test:prod; + +interface ext { + record point { + x: s32, + } +} + +interface defs { + use ext.{point}; + + record region { + inner: point, + } +} + +world prod { + import ext; + export defs; + use defs.{region}; + export make: func() -> region; +} From 1f06572eb925dfc92eebf1b55b36831ab08cff4e Mon Sep 17 00:00:00 2001 From: Salman Saghafi Date: Fri, 12 Jun 2026 23:58:44 -0400 Subject: [PATCH 2/2] fix: alias instance-exported types reached by id in aggregated imports Top-level aggregated imports carry no `uses` map, so a type referenced by id (e.g. an imported instance's export used in a record field) was re-encoded as an invalid local definition. `value_type` now aliases it from the imported instance, and imports are encoded instances-first. --- crates/wac-graph/src/encoding.rs | 29 +++++++++++++++ crates/wac-graph/src/graph.rs | 8 +++-- .../implicit-resource-import/encoded.wat | 21 ++++++----- .../graphs/use-type-by-id-import/encoded.wat | 36 +++++++++++++++++++ .../graphs/use-type-by-id-import/graph.json | 12 +++++++ .../tests/graphs/use-type-by-id-import/m.wat | 28 +++++++++++++++ .../tests/encoding/instantiation.wac.result | 16 ++++----- 7 files changed, 129 insertions(+), 21 deletions(-) create mode 100644 crates/wac-graph/tests/graphs/use-type-by-id-import/encoded.wat create mode 100644 crates/wac-graph/tests/graphs/use-type-by-id-import/graph.json create mode 100644 crates/wac-graph/tests/graphs/use-type-by-id-import/m.wat diff --git a/crates/wac-graph/src/encoding.rs b/crates/wac-graph/src/encoding.rs index 64a4f8e..2b10b02 100644 --- a/crates/wac-graph/src/encoding.rs +++ b/crates/wac-graph/src/encoding.rs @@ -190,6 +190,31 @@ impl<'a> TypeEncoder<'a> { Self(types) } + // Aliases a value type from an imported instance that exports it, rather + // than re-encoding it locally: a type import must reference a named type, + // not a local definition. + fn alias_from_instance_export(&self, state: &mut State, ty: ValueType) -> Option { + if state.current.instances.is_empty() { + return None; + } + let ty = Type::Value(ty); + let (instance, name) = self.0.interfaces().find_map(|interface| { + let iid = interface.id.as_ref()?; + let instance = *state.current.instances.get(iid)?; + interface.exports.iter().find_map(|(name, kind)| { + (*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, + }); + state.current.type_indexes.insert(ty, index); + Some(index) + } + pub fn ty(&self, state: &mut State, ty: Type, name: Option<&str>) -> u32 { if let Some(index) = state.current.type_indexes.get(&ty) { return *index; @@ -532,6 +557,10 @@ impl<'a> TypeEncoder<'a> { return ComponentValType::Type(*index); } + if let Some(index) = self.alias_from_instance_export(state, ty) { + return ComponentValType::Type(index); + } + let index = match ty { ValueType::Primitive(ty) => return ComponentValType::Primitive(ty.into()), ValueType::Borrow(id) => self.borrow(state, id), diff --git a/crates/wac-graph/src/graph.rs b/crates/wac-graph/src/graph.rs index 1b71b6f..ace9cba 100644 --- a/crates/wac-graph/src/graph.rs +++ b/crates/wac-graph/src/graph.rs @@ -1544,8 +1544,12 @@ impl<'a> CompositionGraphEncoder<'a> { let mut encoded = HashMap::new(); - // Next encode the imports - for (name, kind) in aggregator.imports() { + // Instances first, so a later type import can alias a type from an + // instance export instead of re-encoding it as a local definition. + let (instances, rest): (Vec<_>, Vec<_>) = aggregator + .imports() + .partition(|(_, kind)| matches!(kind, ItemKind::Instance(_))); + for (name, kind) in instances.into_iter().chain(rest) { log::debug!("import `{name}` is being imported"); let index = self.import(state, name, aggregator.types(), kind); encoded.insert(name, (kind.into(), index)); diff --git a/crates/wac-graph/tests/graphs/implicit-resource-import/encoded.wat b/crates/wac-graph/tests/graphs/implicit-resource-import/encoded.wat index 0d9ecc0..d238c9c 100644 --- a/crates/wac-graph/tests/graphs/implicit-resource-import/encoded.wat +++ b/crates/wac-graph/tests/graphs/implicit-resource-import/encoded.wat @@ -6,19 +6,18 @@ ) (import "foo:dependency/types" (instance (;0;) (type 0))) (alias export 0 "x" (type (;1;))) - (import "x" (type (;2;) (eq 1))) - (alias export 0 "x" (type (;3;))) - (type (;4;) + (type (;2;) (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;) "my-func" (func (type 3))) ) ) - (import "foo:test-import/my-interface" (instance (;1;) (type 4))) - (type (;5;) + (import "foo:test-import/my-interface" (instance (;1;) (type 2))) + (import "x" (type (;3;) (eq 1))) + (type (;4;) (component (type (;0;) (instance @@ -28,11 +27,11 @@ (export (;0;) "foo:dependency/types" (instance (type 0))) ) ) - (import "unlocked-dep=" (component (;0;) (type 5))) + (import "unlocked-dep=" (component (;0;) (type 4))) (instance (;2;) (instantiate 0)) (alias export 1 "my-func" (func (;0;))) (alias export 2 "foo:dependency/types" (instance (;3;))) - (type (;6;) + (type (;5;) (component (type (;0;) (instance @@ -40,18 +39,18 @@ ) ) (import "foo:dependency/types" (instance (;0;) (type 0))) - (alias outer 1 3 (type (;1;))) + (alias outer 1 1 (type (;1;))) (import "x" (type (;2;) (eq 1))) (type (;3;) (own 2)) (type (;4;) (func (result 3))) (import "my-func" (func (;0;) (type 4))) ) ) - (import "unlocked-dep=" (component (;1;) (type 6))) + (import "unlocked-dep=" (component (;1;) (type 5))) (instance (;4;) (instantiate 1 (with "foo:dependency/types" (instance 3)) (with "my-func" (func 0)) - (with "x" (type 2)) + (with "x" (type 3)) ) ) ) diff --git a/crates/wac-graph/tests/graphs/use-type-by-id-import/encoded.wat b/crates/wac-graph/tests/graphs/use-type-by-id-import/encoded.wat new file mode 100644 index 0000000..7c48934 --- /dev/null +++ b/crates/wac-graph/tests/graphs/use-type-by-id-import/encoded.wat @@ -0,0 +1,36 @@ +(component + (type (;0;) + (instance + (type (;0;) (record (field "x" s32))) + (export (;1;) "point" (type (eq 0))) + ) + ) + (import "test:lib/shapes" (instance (;0;) (type 0))) + (alias export 0 "point" (type (;1;))) + (type (;2;) (record (field "origin" 1))) + (import "region" (type (;3;) (eq 2))) + (type (;4;) + (component + (type (;0;) + (instance + (type (;0;) (record (field "x" s32))) + (export (;1;) "point" (type (eq 0))) + ) + ) + (import "test:lib/shapes" (instance (;0;) (type 0))) + (alias export 0 "point" (type (;1;))) + (type (;2;) (record (field "origin" 1))) + (import "region" (type (;3;) (eq 2))) + (type (;4;) (func (result s32))) + (export (;0;) "run" (func (type 4))) + ) + ) + (import "unlocked-dep=" (component (;0;) (type 4))) + (instance (;1;) (instantiate 0 + (with "test:lib/shapes" (instance 0)) + (with "region" (type 3)) + ) + ) + (alias export 1 "run" (func (;0;))) + (export (;1;) "run" (func 0)) +) diff --git a/crates/wac-graph/tests/graphs/use-type-by-id-import/graph.json b/crates/wac-graph/tests/graphs/use-type-by-id-import/graph.json new file mode 100644 index 0000000..ed6c12a --- /dev/null +++ b/crates/wac-graph/tests/graphs/use-type-by-id-import/graph.json @@ -0,0 +1,12 @@ +{ + "packages": [ + { "name": "test:m", "path": "m.wat" } + ], + "nodes": [ + { "type": "instantiation", "package": 0 }, + { "type": "alias", "source": 0, "export": "run" } + ], + "exports": [ + { "node": 1, "name": "run" } + ] +} diff --git a/crates/wac-graph/tests/graphs/use-type-by-id-import/m.wat b/crates/wac-graph/tests/graphs/use-type-by-id-import/m.wat new file mode 100644 index 0000000..31737c0 --- /dev/null +++ b/crates/wac-graph/tests/graphs/use-type-by-id-import/m.wat @@ -0,0 +1,28 @@ +(component + ;; Origin interface `shapes`, exporting `point`. + (type (;0;) + (instance + (type (;0;) (record (field "x" s32))) + (export (;1;) "point" (type (eq 0))) + ) + ) + (import "test:lib/shapes" (instance (;0;) (type 0))) + (alias export 0 "point" (type (;1;))) + + ;; A record whose field references `point` from `shapes` by id. wit-component + ;; produces this shape for a world that imports `shapes` and `use`s a record + ;; built on `point`. + (type (;2;) (record (field "origin" 1))) + (import "region" (type (;3;) (eq 2))) + + (core module (;0;) + (type (;0;) (func (result i32))) + (func (;0;) (type 0) (result i32) i32.const 7) + (export "run" (func 0)) + ) + (core instance (;0;) (instantiate 0)) + (alias core export 0 "run" (core func (;0;))) + (type (;4;) (func (result s32))) + (func (;0;) (type 4) (canon lift (core func 0))) + (export "run" (func 0)) +) diff --git a/crates/wac-parser/tests/encoding/instantiation.wac.result b/crates/wac-parser/tests/encoding/instantiation.wac.result index f4b72af..891ef23 100644 --- a/crates/wac-parser/tests/encoding/instantiation.wac.result +++ b/crates/wac-parser/tests/encoding/instantiation.wac.result @@ -1,27 +1,27 @@ (component - (type (;0;) (func)) - (import "baz" (func (;0;) (type 0))) - (type (;1;) + (type (;0;) (instance (type (;0;) (func)) (export (;0;) "foo" (func (type 0))) ) ) - (import "foo" (instance (;0;) (type 1))) - (type (;2;) + (import "foo" (instance (;0;) (type 0))) + (type (;1;) (instance (type (;0;) (func)) (export (;0;) "foo" (func (type 0))) ) ) - (import "i" (instance $i (;1;) (type 2))) - (type (;3;) + (import "i" (instance $i (;1;) (type 1))) + (type (;2;) (instance (type (;0;) (func)) (export (;0;) "baz" (func (type 0))) ) ) - (import "i2" (instance $i2 (;2;) (type 3))) + (import "i2" (instance $i2 (;2;) (type 2))) + (type (;3;) (func)) + (import "baz" (func (;0;) (type 3))) (type (;4;) (func)) (import "f" (func $f (;1;) (type 4))) (type (;5;)