From 21fc7276e749c916d3357d2ee5be79504f813243 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 9 Feb 2026 16:22:30 -0800 Subject: [PATCH 1/4] Add a new `Resolve::generate_nominal_type_ids` method This commit adds a new method to `Resolve` which is intended to resolve a longstanding issue for bindings generators based on `wit-parser`: #1497. Specifically this adds functionality to `Resolve` to duplicate exported interfaces and their contents, if necessary. This is a boon to bindings generators because it means that a `TypeId`, for example, uniquely identifies a single generated type. Previously it might refer to one of two types, either the imported version or the exported version. After this method, however, there will be two `TypeId`s if necessary. This is currently modeled as a mutation to `Resolve` which is opt-in. This is done to avoid tampering with the AST-like structure of `Resolve` today where other AST-like operations don't want to necessarily have to keep everything in sync. Once a `Resolve` is nominalized, however, it's effectively incompatible with other operations such as merging, printing, etc. My thinking is that for now this is a reasonable tradeoff as bindings generators can pretty easily invoke this method before actually running bindings generation. Closes #1497 --- Cargo.lock | 82 ++++++---- Cargo.toml | 4 +- crates/wit-parser/src/ast/resolve.rs | 1 + crates/wit-parser/src/decoding.rs | 2 + crates/wit-parser/src/lib.rs | 12 ++ crates/wit-parser/src/resolve/clone.rs | 39 +++-- crates/wit-parser/src/resolve/mod.rs | 148 +++++++++++++++++- fuzz/src/roundtrip_wit.rs | 5 + src/bin/wasm-tools/component.rs | 40 ++++- tests/cli/help-component-wit-short.wat.stdout | 2 + tests/cli/help-component-wit.wat.stdout | 11 ++ tests/cli/nominal1.wit | 13 ++ tests/cli/nominal1.wit.stdout | 103 ++++++++++++ tests/cli/nominal2.wit | 17 ++ tests/cli/nominal2.wit.stdout | 143 +++++++++++++++++ tests/cli/nominal3.wit | 18 +++ tests/cli/nominal3.wit.stdout | 103 ++++++++++++ 17 files changed, 685 insertions(+), 58 deletions(-) create mode 100644 tests/cli/nominal1.wit create mode 100644 tests/cli/nominal1.wit.stdout create mode 100644 tests/cli/nominal2.wit create mode 100644 tests/cli/nominal2.wit.stdout create mode 100644 tests/cli/nominal3.wit create mode 100644 tests/cli/nominal3.wit.stdout diff --git a/Cargo.lock b/Cargo.lock index 0f6b3025c9..5c1b4617dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -784,6 +784,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -913,7 +919,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" dependencies = [ "fallible-iterator", - "indexmap 2.10.0", + "indexmap 2.13.0", "stable_deref_trait", ] @@ -954,8 +960,19 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ - "foldhash", + "foldhash 0.1.5", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", "serde", + "serde_core", ] [[package]] @@ -1141,13 +1158,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] @@ -1438,7 +1456,7 @@ dependencies = [ "crc32fast", "flate2", "hashbrown 0.15.4", - "indexmap 2.10.0", + "indexmap 2.13.0", "memchr", "ruzstd", ] @@ -1474,7 +1492,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.10.0", + "indexmap 2.13.0", ] [[package]] @@ -1827,7 +1845,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.13.0", "itoa", "ryu", "serde", @@ -2153,7 +2171,7 @@ dependencies = [ "glob", "heck", "im-rc", - "indexmap 2.10.0", + "indexmap 2.13.0", "log", "petgraph", "pretty_assertions", @@ -2216,7 +2234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "865c5bff5f7a3781b5f92ea4cfa99bb38267da097441cdb09080de1568ef3075" dependencies = [ "anyhow", - "indexmap 2.10.0", + "indexmap 2.13.0", "serde", "serde_derive", "serde_json", @@ -2232,7 +2250,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae05bf9579f45a62e8d0a4e3f52eaa8da518883ac5afa482ec8256c329ecd56" dependencies = [ "anyhow", - "indexmap 2.10.0", + "indexmap 2.13.0", "wasm-encoder 0.243.0", "wasmparser 0.243.0", ] @@ -2245,7 +2263,7 @@ dependencies = [ "auditable-serde", "clap", "flate2", - "indexmap 2.10.0", + "indexmap 2.13.0", "serde", "serde_derive", "serde_json", @@ -2340,7 +2358,7 @@ dependencies = [ "cpp_demangle", "env_logger", "gimli", - "indexmap 2.10.0", + "indexmap 2.13.0", "is_executable", "json-from-wast", "libtest-mimic", @@ -2430,7 +2448,7 @@ dependencies = [ "ahash", "bitflags", "hashbrown 0.14.5", - "indexmap 2.10.0", + "indexmap 2.13.0", "semver", ] @@ -2442,7 +2460,7 @@ checksum = "b51cb03afce7964bbfce46602d6cb358726f36430b6ba084ac6020d8ce5bc102" dependencies = [ "bitflags", "hashbrown 0.15.4", - "indexmap 2.10.0", + "indexmap 2.13.0", "semver", "serde", ] @@ -2455,7 +2473,7 @@ checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d" dependencies = [ "bitflags", "hashbrown 0.15.4", - "indexmap 2.10.0", + "indexmap 2.13.0", "semver", ] @@ -2467,8 +2485,8 @@ dependencies = [ "bitflags", "criterion", "env_logger", - "hashbrown 0.15.4", - "indexmap 2.10.0", + "hashbrown 0.16.1", + "indexmap 2.13.0", "log", "once_cell", "rayon", @@ -2514,7 +2532,7 @@ dependencies = [ "cfg-if", "encoding_rs", "hashbrown 0.15.4", - "indexmap 2.10.0", + "indexmap 2.13.0", "libc", "log", "mach2", @@ -2612,7 +2630,7 @@ dependencies = [ "cranelift-bitset", "cranelift-entity", "gimli", - "indexmap 2.10.0", + "indexmap 2.13.0", "log", "object", "postcard", @@ -2706,7 +2724,7 @@ checksum = "f967f5efaaac7694e6bd0d67542a5a036830860e4adf95684260181e85a5d299" dependencies = [ "anyhow", "heck", - "indexmap 2.10.0", + "indexmap 2.13.0", "wit-parser 0.233.0", ] @@ -2975,7 +2993,7 @@ source = "git+https://github.com/bytecodealliance/wit-bindgen#f1d72d49a60524263e dependencies = [ "anyhow", "heck", - "indexmap 2.10.0", + "indexmap 2.13.0", "prettyplease", "syn", "wasm-metadata 0.243.0", @@ -3005,7 +3023,7 @@ checksum = "fd9fd46f0e783bf80f1ab7291f9d442fa5553ff0e96cdb71964bd8859b734b55" dependencies = [ "anyhow", "bitflags", - "indexmap 2.10.0", + "indexmap 2.13.0", "log", "serde", "serde_derive", @@ -3024,7 +3042,7 @@ checksum = "36f9fc53513e461ce51dcf17a3e331752cb829f1d187069e54af5608fc998fe4" dependencies = [ "anyhow", "bitflags", - "indexmap 2.10.0", + "indexmap 2.13.0", "log", "serde", "serde_derive", @@ -3043,7 +3061,7 @@ dependencies = [ "bitflags", "env_logger", "glob", - "indexmap 2.10.0", + "indexmap 2.13.0", "libtest-mimic", "log", "pretty_assertions", @@ -3070,7 +3088,7 @@ dependencies = [ "artifacts", "clap", "env_logger", - "indexmap 2.10.0", + "indexmap 2.13.0", "libtest-mimic", "tempfile", "wasm-encoder 0.244.0", @@ -3108,7 +3126,7 @@ checksum = "681d526d6ea42e28f9afe9eae2b50e0b0a627aef8822c75eb04078db84d03e57" dependencies = [ "anyhow", "id-arena", - "indexmap 2.10.0", + "indexmap 2.13.0", "log", "semver", "serde", @@ -3126,7 +3144,7 @@ checksum = "f22f1cd55247a2e616870b619766e9522df36b7abafbb29bbeb34b7a9da7e9f0" dependencies = [ "anyhow", "id-arena", - "indexmap 2.10.0", + "indexmap 2.13.0", "log", "semver", "serde", @@ -3144,7 +3162,7 @@ checksum = "df983a8608e513d8997f435bb74207bf0933d0e49ca97aa9d8a6157164b9b7fc" dependencies = [ "anyhow", "id-arena", - "indexmap 2.10.0", + "indexmap 2.13.0", "log", "semver", "serde", @@ -3160,9 +3178,9 @@ version = "0.244.0" dependencies = [ "anyhow", "env_logger", - "hashbrown 0.15.4", + "hashbrown 0.16.1", "id-arena", - "indexmap 2.10.0", + "indexmap 2.13.0", "libtest-mimic", "log", "pretty_assertions", @@ -3194,7 +3212,7 @@ version = "0.244.0" dependencies = [ "arbitrary", "clap", - "indexmap 2.10.0", + "indexmap 2.13.0", "log", "semver", "wasmparser 0.244.0", diff --git a/Cargo.toml b/Cargo.toml index 315bc77afb..60a4550b5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,9 +118,9 @@ comfy-table = { version = "7.1.3", default-features = false } criterion = { version = "0.5.1", default-features = false } env_logger = "0.11" gimli = "0.31.1" -hashbrown = { version = "0.15.2", default-features = false, features = ['default-hasher'] } +hashbrown = { version = "0.16.1", default-features = false, features = ['default-hasher'] } id-arena = { version = "2.3.0", default-features = false } -indexmap = { version = "2.7.0", default-features = false } +indexmap = { version = "2.13.0", default-features = false } indoc = "2.0.5" leb128fmt = { version = "0.1.0", default-features = false } libfuzzer-sys = "0.4.0" diff --git a/crates/wit-parser/src/ast/resolve.rs b/crates/wit-parser/src/ast/resolve.rs index 0b60033497..e77ac0f260 100644 --- a/crates/wit-parser/src/ast/resolve.rs +++ b/crates/wit-parser/src/ast/resolve.rs @@ -313,6 +313,7 @@ impl<'a> Resolver<'a> { functions: IndexMap::default(), package: None, span, + clone_of: None, }) } diff --git a/crates/wit-parser/src/decoding.rs b/crates/wit-parser/src/decoding.rs index 5156d115b6..88253a3b6e 100644 --- a/crates/wit-parser/src/decoding.rs +++ b/crates/wit-parser/src/decoding.rs @@ -912,6 +912,7 @@ impl WitPackageDecoder<'_> { package: None, stability: Default::default(), span: Default::default(), + clone_of: None, }) }); @@ -968,6 +969,7 @@ impl WitPackageDecoder<'_> { package: None, stability: Default::default(), span: Default::default(), + clone_of: None, }; let owner = TypeOwner::Interface(self.resolve.interfaces.next_id()); diff --git a/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs index 71189324fc..132dbc99e8 100644 --- a/crates/wit-parser/src/lib.rs +++ b/crates/wit-parser/src/lib.rs @@ -690,6 +690,18 @@ pub struct Interface { /// Source span for this interface. #[cfg_attr(feature = "serde", serde(skip))] pub span: Span, + + /// The interface that this one was cloned from, if any. + /// + /// Applicable for [`Resolve::generate_nominal_type_ids`]. + #[cfg_attr( + feature = "serde", + serde( + skip_serializing_if = "Option::is_none", + serialize_with = "serialize_optional_id", + ) + )] + pub clone_of: Option, } impl Interface { diff --git a/crates/wit-parser/src/resolve/clone.rs b/crates/wit-parser/src/resolve/clone.rs index 12d02ed419..8c9b389923 100644 --- a/crates/wit-parser/src/resolve/clone.rs +++ b/crates/wit-parser/src/resolve/clone.rs @@ -45,14 +45,10 @@ impl CloneMaps { pub struct Cloner<'a> { pub resolve: &'a mut Resolve, + pub maps: &'a mut CloneMaps, prev_owner: TypeOwner, new_owner: TypeOwner, - /// This map keeps track, in the current scope of types, of all copied - /// types. This deduplicates copying types to ensure that they're only - /// copied at most once. - pub types: HashMap, - /// If `None` then it's inferred from `self.new_owner`. pub new_package: Option, } @@ -60,6 +56,7 @@ pub struct Cloner<'a> { impl<'a> Cloner<'a> { pub fn new( resolve: &'a mut Resolve, + maps: &'a mut CloneMaps, prev_owner: TypeOwner, new_owner: TypeOwner, ) -> Cloner<'a> { @@ -67,7 +64,7 @@ impl<'a> Cloner<'a> { prev_owner, new_owner, resolve, - types: Default::default(), + maps, new_package: None, } } @@ -83,11 +80,11 @@ impl<'a> Cloner<'a> { let Some(WorldItem::Type { id: from_id, .. }) = from.imports.get(name) else { continue; }; - self.types.insert(*from_id, *into_id); + self.maps.types.insert(*from_id, *into_id); } } - pub fn world_item(&mut self, key: &WorldKey, item: &mut WorldItem, clone_maps: &mut CloneMaps) { + pub fn world_item(&mut self, key: &WorldKey, item: &mut WorldItem) { match key { WorldKey::Name(_) => {} WorldKey::Interface(_) => return, @@ -101,21 +98,19 @@ impl<'a> Cloner<'a> { self.function(f); } WorldItem::Interface { id, .. } => { - let old = *id; - self.interface(id, &mut clone_maps.types); - clone_maps.interfaces.insert(old, *id); + self.interface(id); } } } fn type_id(&mut self, ty: &mut TypeId) { - if !self.types.contains_key(ty) { + if !self.maps.types.contains_key(ty) { let mut new = self.resolve.types[*ty].clone(); self.type_def(&mut new); let id = self.resolve.types.alloc(new); - self.types.insert(*ty, id); + self.maps.types.insert(*ty, id); } - *ty = self.types[&*ty]; + *ty = self.maps.types[&*ty]; } fn type_def(&mut self, def: &mut TypeDef) { @@ -127,8 +122,15 @@ impl<'a> Cloner<'a> { TypeDefKind::Type(Type::Id(id)) => { if self.resolve.types[*id].owner == self.prev_owner { self.type_id(id); + } else if let Some(new_id) = self.maps.types.get(id) { + *id = *new_id; } else { - // .. + // This type isn't owned by `self.prev_owner`, nor is there + // a listed mapping for it. This most likely means that + // `def` is equivalent to a `use` importing from another + // interface and `id` is the type being imported. In this + // situation it's left as-is to continue importing from + // that interface. } } TypeDefKind::Type(_) @@ -200,11 +202,12 @@ impl<'a> Cloner<'a> { } } - fn interface(&mut self, id: &mut InterfaceId, cloned_types: &mut HashMap) { + pub fn interface(&mut self, id: &mut InterfaceId) { let mut new = self.resolve.interfaces[*id].clone(); let next_id = self.resolve.interfaces.next_id(); let mut clone = Cloner::new( self.resolve, + self.maps, TypeOwner::Interface(*id), TypeOwner::Interface(next_id), ); @@ -214,13 +217,15 @@ impl<'a> Cloner<'a> { for func in new.functions.values_mut() { clone.function(func); } - cloned_types.extend(clone.types); 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::None => unreachable!(), })); + new.clone_of = Some(*id); *id = self.resolve.interfaces.alloc(new); + let prev = self.maps.interfaces.insert(*id, next_id); + assert!(prev.is_none()); assert_eq!(*id, next_id); } } diff --git a/crates/wit-parser/src/resolve/mod.rs b/crates/wit-parser/src/resolve/mod.rs index e4800c3a57..8199afcfd7 100644 --- a/crates/wit-parser/src/resolve/mod.rs +++ b/crates/wit-parser/src/resolve/mod.rs @@ -832,14 +832,17 @@ package {name} is defined in two different locations:\n\ // Cloning is no trivial task, however, so cloning is delegated to a // submodule to perform a "deep" clone and copy items into new arena // entries as necessary. - let mut cloner = clone::Cloner::new(self, TypeOwner::World(from), TypeOwner::World(into)); + let mut cloner = clone::Cloner::new( + self, + clone_maps, + TypeOwner::World(from), + TypeOwner::World(into), + ); cloner.register_world_type_overlap(from, into); for (name, item) in new_imports.iter_mut().chain(&mut new_exports) { - cloner.world_item(name, item, clone_maps); + cloner.world_item(name, item); } - clone_maps.types.extend(cloner.types); - // Insert any new imports and new exports found first. let into_world = &mut self.worlds[into]; for (name, import) in new_imports { @@ -1515,7 +1518,14 @@ package {name} is defined in two different locations:\n\ for (id, iface) in self.interfaces.iter() { assert!(self.packages.get(iface.package.unwrap()).is_some()); if iface.name.is_some() { - assert!(package_interfaces[iface.package.unwrap().index()].contains(&id)); + match iface.clone_of { + Some(other) => { + assert_eq!(iface.name, self.interfaces[other].name); + } + None => { + assert!(package_interfaces[iface.package.unwrap().index()].contains(&id)); + } + } } for (name, ty) in iface.types.iter() { @@ -2543,6 +2553,130 @@ package {name} is defined in two different locations:\n\ }, } } + + /// This method will rewrite the `world` provided to ensure that, where + /// necessary, all types in interfaces referred to by the `world` have + /// nominal type ids for bindings generation. + /// + /// The need for this method primarily arises from bindings generators + /// generating types in a programming language. Bindings generators try to + /// generate a type-per-WIT-type but this becomes problematic in situations + /// such as when an `interface` is both imported and exported. For example: + /// + /// ```wit + /// interface x { + /// resource r; + /// } + /// + /// world foo { + /// import x; + /// export x; + /// } + /// ``` + /// + /// Here the `r` resource, before this method, exists once within this + /// [`Resolve`]. This is a problem for bindings generators because guest + /// languages typically want to represent this world with two types: one + /// for the import and one for the export. This matches component model + /// semantics where `r` is a different type between the import and the + /// export. + /// + /// The purpose of this method is to ensure that languages with nominal + /// types, where type identity is unique based on definition not structure, + /// will have an easier time generating bindings. This method will + /// duplicate the interface `x`, for example, and everything it contains. + /// This means that the `world foo` above will have a different + /// `InterfaceId` for the import and the export of `x`, despite them using + /// the same interface in WIT. This is intended to make bindings generators' + /// jobs much easier because now Id-uniqueness matches the semantic meaning + /// of the world as well. + /// + /// This function will rewrite exported interfaces, as appropriate, to all + /// have unique ids if they would otherwise overlap with the imports. + pub fn generate_nominal_type_ids(&mut self, world: WorldId) { + let mut imports = HashSet::new(); + + // Build up a list of all imported interfaces, they're not changing and + // this is used to test for overlap between imports/exports. + for import in self.worlds[world].imports.values() { + if let WorldItem::Interface { id, .. } = import { + imports.insert(*id); + } + } + + let mut to_clone = IndexMap::default(); + for (i, export) in self.worlds[world].exports.values().enumerate() { + let id = match export { + WorldItem::Interface { id, .. } => *id, + + // Functions can only refer to imported types so there's no need + // to rewrite anything as imports always stay as-is. + WorldItem::Function(_) => continue, + + WorldItem::Type { .. } => unreachable!(), + }; + + // If this interface itself is both imported and exported, or if any + // dependency of this interface is rewritten, then the interface + // itself needs to be rewritten. Otherwise continue onwards. + let imported_and_exported = imports.contains(&id); + let any_dep_rewritten = self + .interface_direct_deps(id) + .any(|dep| to_clone.contains_key(&dep)); + if !(imported_and_exported || any_dep_rewritten) { + continue; + } + + to_clone.insert(id, i); + } + + let mut maps = CloneMaps::default(); + let mut cloner = clone::Cloner::new( + self, + &mut maps, + TypeOwner::World(world), + TypeOwner::World(world), + ); + for (id, i) in to_clone { + // First, clone the interface. This'll make a `new_id`, and then we + // need to update the world to point to this new id. Note that the + // clones happen topologically here (due to iterating in-order + // above) and the `CloneMaps` are shared amongst interfaces. This + // means that future clones will use the types produced here too. + let mut new_id = id; + cloner.interface(&mut new_id); + + // Load up the previous `key` and go ahead and mutate the + // `WorldItem` in place which is guaranteed to be an `Interface` + // because of the loop above. + let exports = &mut cloner.resolve.worlds[world].exports; + let (key, prev) = exports.get_index_mut(i).unwrap(); + match prev { + WorldItem::Interface { id, .. } => *id = new_id, + _ => unreachable!(), + } + + match key { + // If the key for this is an `Interface` then that means we + // need to update the key as well. Here that's replaced by-index + // in the `IndexMap` to preserve the same ordering as before, + // and this operation should always succeed since `new_id` is + // fresh, hence the `unwrap()`. + WorldKey::Interface(_) => { + exports + .replace_index(i, WorldKey::Interface(new_id)) + .unwrap(); + } + + // Name-based keys don't need updating as they only contain a + // string, no ids. + WorldKey::Name(_) => {} + } + } + + #[cfg(debug_assertions)] + self.assert_valid(); + } } /// Possible imports that can be passed to [`Resolve::wasm_import_name`]. @@ -3511,8 +3645,10 @@ impl Remap { )); } + let mut maps = Default::default(); let mut cloner = clone::Cloner::new( resolve, + &mut maps, TypeOwner::World(if is_external_include { include_world_id } else { @@ -3574,7 +3710,7 @@ impl Remap { // in the function itself. let mut new_item = item.1.clone(); let key = WorldKey::Name(n.clone()); - cloner.world_item(&key, &mut new_item, &mut CloneMaps::default()); + cloner.world_item(&key, &mut new_item); match &mut new_item { WorldItem::Function(f) => f.name = n.clone(), WorldItem::Type { id, .. } => cloner.resolve.types[*id].name = Some(n.clone()), diff --git a/fuzz/src/roundtrip_wit.rs b/fuzz/src/roundtrip_wit.rs index 0cfdfa7172..480093bb64 100644 --- a/fuzz/src/roundtrip_wit.rs +++ b/fuzz/src/roundtrip_wit.rs @@ -90,6 +90,11 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> { log::debug!("... importizing this world"); let mut resolve2 = resolve.clone(); let _ = resolve2.importize(id, None); + + // Test out `generate_nominal_type_ids` + log::debug!("... calling `generate_nominal_type_ids`"); + let mut resolve2 = resolve.clone(); + resolve2.generate_nominal_type_ids(id); } if decoded_bindgens.len() < 2 { diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs index 84375ef0bc..2881e2ddf2 100644 --- a/src/bin/wasm-tools/component.rs +++ b/src/bin/wasm-tools/component.rs @@ -761,7 +761,8 @@ pub struct WitOpts { #[clap( long, conflicts_with = "importize_world", - conflicts_with = "merge_world_imports_based_on_semver" + conflicts_with = "merge_world_imports_based_on_semver", + conflicts_with = "generate_nominal_type_ids" )] importize: bool, @@ -783,6 +784,7 @@ pub struct WitOpts { long, conflicts_with = "importize", conflicts_with = "merge_world_imports_based_on_semver", + conflicts_with = "generate_nominal_type_ids", value_name = "WORLD" )] importize_world: Option, @@ -798,10 +800,29 @@ pub struct WitOpts { long, conflicts_with = "importize", conflicts_with = "importize_world", + conflicts_with = "generate_nominal_type_ids", value_name = "WORLD" )] merge_world_imports_based_on_semver: Option, + /// Generates unique type IDs for nominal types in the world provided. + /// + /// This option can be used to affect the `--json` output of this command, + /// for example, and it is intended to make the job of bindings generators + /// easier by ensuring that each `TypeId` corresponds to a single type to + /// generate in the target language. This will modify the `world` specified + /// to duplicate exported interfaces, if needed, to ensure that if they're + /// both imported and exported it generates unique types in the guest + /// language. + #[clap( + long, + conflicts_with = "importize", + conflicts_with = "importize_world", + conflicts_with = "merge_world_imports_based_on_semver", + value_name = "WORLD" + )] + generate_nominal_type_ids: Option, + /// Features to enable when parsing the `wit` option. /// /// This flag enables the `@unstable` feature in WIT documents where the @@ -852,6 +873,8 @@ impl WitOpts { .context("failed to merge world imports based on semver")?; let resolve = mem::take(resolve); decoded = DecodedWasm::Component(resolve, world_id); + } else if let Some(world) = &self.generate_nominal_type_ids { + self.generate_nominal_type_ids(&mut decoded, world)?; } // Now that the WIT document has been decoded, it's time to emit it. @@ -963,6 +986,21 @@ impl WitOpts { Ok(()) } + fn generate_nominal_type_ids(&self, decoded: &mut DecodedWasm, world: &str) -> Result<()> { + let (resolve, pkg) = match decoded { + DecodedWasm::Component(resolve, world_id) => { + let pkg = resolve.worlds[*world_id].package.unwrap(); + (resolve, pkg) + } + DecodedWasm::WitPackage(resolve, pkg) => (resolve, *pkg), + }; + let world_id = resolve.select_world(&[pkg], Some(world))?; + resolve.generate_nominal_type_ids(world_id); + let resolve = mem::take(resolve); + *decoded = DecodedWasm::Component(resolve, world_id); + Ok(()) + } + fn emit_wasm(&self, decoded: &DecodedWasm) -> Result<()> { assert!(self.wasm || self.wat); assert!(self.out_dir.is_none()); diff --git a/tests/cli/help-component-wit-short.wat.stdout b/tests/cli/help-component-wit-short.wat.stdout index 1c8e4fd710..bc175bdafb 100644 --- a/tests/cli/help-component-wit-short.wat.stdout +++ b/tests/cli/help-component-wit-short.wat.stdout @@ -40,6 +40,8 @@ Options: --merge-world-imports-based-on-semver Updates the world specified to deduplicate all of its imports based on semver versions + --generate-nominal-type-ids + Generates unique type IDs for nominal types in the world provided --features Features to enable when parsing the `wit` option --all-features diff --git a/tests/cli/help-component-wit.wat.stdout b/tests/cli/help-component-wit.wat.stdout index cf46af0a65..214c6da183 100644 --- a/tests/cli/help-component-wit.wat.stdout +++ b/tests/cli/help-component-wit.wat.stdout @@ -102,6 +102,17 @@ Options: can be used to explore outside of that command what's happening to the WIT. + --generate-nominal-type-ids + Generates unique type IDs for nominal types in the world provided. + + This option can be used to affect the `--json` output of this command, + for example, and it is intended to make the job of bindings generators + easier by ensuring that each `TypeId` corresponds to a single type to + generate in the target language. This will modify the `world` + specified to duplicate exported interfaces, if needed, to ensure that + if they're both imported and exported it generates unique types in the + guest language. + --features Features to enable when parsing the `wit` option. diff --git a/tests/cli/nominal1.wit b/tests/cli/nominal1.wit new file mode 100644 index 0000000000..5f2f4e1877 --- /dev/null +++ b/tests/cli/nominal1.wit @@ -0,0 +1,13 @@ +// RUN: component wit --generate-nominal-type-ids foo % --json + +package a:b; + +interface x { + resource r; + hi: func() -> r; +} + +world foo { + import x; + export x; +} diff --git a/tests/cli/nominal1.wit.stdout b/tests/cli/nominal1.wit.stdout new file mode 100644 index 0000000000..b9ff3e7604 --- /dev/null +++ b/tests/cli/nominal1.wit.stdout @@ -0,0 +1,103 @@ +{ + "worlds": [ + { + "name": "foo", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "exports": { + "interface-1": { + "interface": { + "id": 1 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "x", + "types": { + "r": 0 + }, + "functions": { + "hi": { + "name": "hi", + "kind": "freestanding", + "params": [], + "result": 1 + } + }, + "package": 0 + }, + { + "name": "x", + "types": { + "r": 2 + }, + "functions": { + "hi": { + "name": "hi", + "kind": "freestanding", + "params": [], + "result": 3 + } + }, + "package": 0, + "clone_of": 0 + } + ], + "types": [ + { + "name": "r", + "kind": "resource", + "owner": { + "interface": 0 + } + }, + { + "name": null, + "kind": { + "handle": { + "own": 0 + } + }, + "owner": null + }, + { + "name": "r", + "kind": "resource", + "owner": { + "interface": 1 + } + }, + { + "name": null, + "kind": { + "handle": { + "own": 2 + } + }, + "owner": null + } + ], + "packages": [ + { + "name": "a:b", + "docs": { + "contents": "RUN: component wit --generate-nominal-type-ids foo % --json" + }, + "interfaces": { + "x": 0 + }, + "worlds": { + "foo": 0 + } + } + ] +} diff --git a/tests/cli/nominal2.wit b/tests/cli/nominal2.wit new file mode 100644 index 0000000000..4ae6ca9db7 --- /dev/null +++ b/tests/cli/nominal2.wit @@ -0,0 +1,17 @@ +// RUN: component wit --generate-nominal-type-ids foo % --json + +package a:b; + +interface x { + resource r; + hi: func() -> r; +} + +world foo { + import x; + export x; + + export y: interface { + use x.{r}; + } +} diff --git a/tests/cli/nominal2.wit.stdout b/tests/cli/nominal2.wit.stdout new file mode 100644 index 0000000000..01060209ce --- /dev/null +++ b/tests/cli/nominal2.wit.stdout @@ -0,0 +1,143 @@ +{ + "worlds": [ + { + "name": "foo", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "exports": { + "interface-2": { + "interface": { + "id": 2 + } + }, + "y": { + "interface": { + "id": 3 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "x", + "types": { + "r": 0 + }, + "functions": { + "hi": { + "name": "hi", + "kind": "freestanding", + "params": [], + "result": 2 + } + }, + "package": 0 + }, + { + "name": null, + "types": { + "r": 1 + }, + "functions": {}, + "package": 0 + }, + { + "name": "x", + "types": { + "r": 3 + }, + "functions": { + "hi": { + "name": "hi", + "kind": "freestanding", + "params": [], + "result": 4 + } + }, + "package": 0, + "clone_of": 0 + }, + { + "name": null, + "types": { + "r": 5 + }, + "functions": {}, + "package": 0, + "clone_of": 1 + } + ], + "types": [ + { + "name": "r", + "kind": "resource", + "owner": { + "interface": 0 + } + }, + { + "name": "r", + "kind": { + "type": 0 + }, + "owner": { + "interface": 1 + } + }, + { + "name": null, + "kind": { + "handle": { + "own": 0 + } + }, + "owner": null + }, + { + "name": "r", + "kind": "resource", + "owner": { + "interface": 2 + } + }, + { + "name": null, + "kind": { + "handle": { + "own": 3 + } + }, + "owner": null + }, + { + "name": "r", + "kind": { + "type": 3 + }, + "owner": { + "interface": 3 + } + } + ], + "packages": [ + { + "name": "a:b", + "docs": { + "contents": "RUN: component wit --generate-nominal-type-ids foo % --json" + }, + "interfaces": { + "x": 0 + }, + "worlds": { + "foo": 0 + } + } + ] +} diff --git a/tests/cli/nominal3.wit b/tests/cli/nominal3.wit new file mode 100644 index 0000000000..a5a7c70f6f --- /dev/null +++ b/tests/cli/nominal3.wit @@ -0,0 +1,18 @@ +// RUN: component wit --generate-nominal-type-ids foo % --json + +package a:b; + +interface x { + resource r; + hi: func() -> r; +} + +interface y { + resource r; + hi: func() -> r; +} + +world foo { + import x; + export y; +} diff --git a/tests/cli/nominal3.wit.stdout b/tests/cli/nominal3.wit.stdout new file mode 100644 index 0000000000..15aaca488d --- /dev/null +++ b/tests/cli/nominal3.wit.stdout @@ -0,0 +1,103 @@ +{ + "worlds": [ + { + "name": "foo", + "imports": { + "interface-0": { + "interface": { + "id": 0 + } + } + }, + "exports": { + "interface-1": { + "interface": { + "id": 1 + } + } + }, + "package": 0 + } + ], + "interfaces": [ + { + "name": "x", + "types": { + "r": 0 + }, + "functions": { + "hi": { + "name": "hi", + "kind": "freestanding", + "params": [], + "result": 2 + } + }, + "package": 0 + }, + { + "name": "y", + "types": { + "r": 1 + }, + "functions": { + "hi": { + "name": "hi", + "kind": "freestanding", + "params": [], + "result": 3 + } + }, + "package": 0 + } + ], + "types": [ + { + "name": "r", + "kind": "resource", + "owner": { + "interface": 0 + } + }, + { + "name": "r", + "kind": "resource", + "owner": { + "interface": 1 + } + }, + { + "name": null, + "kind": { + "handle": { + "own": 0 + } + }, + "owner": null + }, + { + "name": null, + "kind": { + "handle": { + "own": 1 + } + }, + "owner": null + } + ], + "packages": [ + { + "name": "a:b", + "docs": { + "contents": "RUN: component wit --generate-nominal-type-ids foo % --json" + }, + "interfaces": { + "x": 0, + "y": 1 + }, + "worlds": { + "foo": 0 + } + } + ] +} From 9ead1e809b3e865a9c0f2e18fb676a63a4a3703f Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 9 Feb 2026 16:30:52 -0800 Subject: [PATCH 2/4] Update MSRV to 1.82 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 60a4550b5c..9b5244ed2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,7 @@ version = "0.244.0" # message to wasm-tools maintainers to discuss. In some cases it's possible to # add version detection to build scripts but in other cases this may not be # reasonable to expect. -rust-version = "1.81.0" +rust-version = "1.82.0" [workspace.dependencies] ahash = { version = "0.8.11", default-features = false } From 4b3704f1fc7f8d835f9e9843a1895479e18db9a5 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 9 Feb 2026 16:54:25 -0800 Subject: [PATCH 3/4] Fix tests --- crates/wit-dylib/tests/roundtrip.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/wit-dylib/tests/roundtrip.rs b/crates/wit-dylib/tests/roundtrip.rs index 411e49df7d..c32d03881f 100644 --- a/crates/wit-dylib/tests/roundtrip.rs +++ b/crates/wit-dylib/tests/roundtrip.rs @@ -168,6 +168,7 @@ fn run_one(u: &mut Unstructured<'_>) -> Result<()> { funcs }, span: Default::default(), + clone_of: None, }); // Generate two worlds in our custom package, one for the callee and one for From a1d969f4ec7d9c7001edef1e5a928f9dc622e286 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 10 Feb 2026 10:05:55 -0800 Subject: [PATCH 4/4] Review comments --- crates/wit-parser/src/resolve/clone.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/wit-parser/src/resolve/clone.rs b/crates/wit-parser/src/resolve/clone.rs index 8c9b389923..3dc4da75e3 100644 --- a/crates/wit-parser/src/resolve/clone.rs +++ b/crates/wit-parser/src/resolve/clone.rs @@ -203,12 +203,13 @@ impl<'a> Cloner<'a> { } pub fn interface(&mut self, id: &mut InterfaceId) { - let mut new = self.resolve.interfaces[*id].clone(); + let old_id = *id; + let mut new = self.resolve.interfaces[old_id].clone(); let next_id = self.resolve.interfaces.next_id(); let mut clone = Cloner::new( self.resolve, self.maps, - TypeOwner::Interface(*id), + TypeOwner::Interface(old_id), TypeOwner::Interface(next_id), ); for id in new.types.values_mut() { @@ -222,9 +223,9 @@ impl<'a> Cloner<'a> { TypeOwner::World(id) => self.resolve.worlds[id].package.unwrap(), TypeOwner::None => unreachable!(), })); - new.clone_of = Some(*id); + new.clone_of = Some(old_id); *id = self.resolve.interfaces.alloc(new); - let prev = self.maps.interfaces.insert(*id, next_id); + let prev = self.maps.interfaces.insert(old_id, next_id); assert!(prev.is_none()); assert_eq!(*id, next_id); }