From 6e1552a6e1bb274020233c60dddef82033ac8091 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 6 Jul 2026 10:25:53 -0700 Subject: [PATCH] Support `external-id` on more items This commit extends the work done in #2555 to support `@external-id` on all component model items and notably in more locations in WIT. This required mostly work on the WIT side of things to ensure that it's plumbed everywhere. --- crates/wasmparser/src/validator/component.rs | 4 - crates/wit-component/src/encoding.rs | 42 ++++++-- crates/wit-component/src/encoding/types.rs | 30 ++++-- crates/wit-component/src/encoding/wit.rs | 14 +-- crates/wit-component/src/encoding/world.rs | 9 +- crates/wit-component/src/printing.rs | 51 ++++++---- crates/wit-dylib/tests/roundtrip.rs | 8 ++ crates/wit-parser/src/ast/resolve.rs | 10 ++ crates/wit-parser/src/decoding.rs | 96 +++++++++---------- crates/wit-parser/src/lib.rs | 8 ++ crates/wit-parser/src/resolve/mod.rs | 9 +- crates/wit-parser/src/sizealign.rs | 4 + crates/wit-smith/src/generate.rs | 15 ++- tests/cli/component-model/implements.wast | 20 +++- tests/cli/dummy-external-id-nested-items.wit | 27 ++++++ .../dummy-external-id-nested-items.wit.stdout | 24 +++++ tests/cli/dummy-external-id-world-items.wit | 15 +++ .../dummy-external-id-world-items.wit.stdout | 11 +++ tests/cli/dummy-external-id2.wit | 12 --- tests/cli/dummy-external-id2.wit.stdout | 11 --- tests/cli/external-id-in-binary-format.wit | 45 +++++++++ .../external-id-in-binary-format.wit.stdout | 40 ++++++++ tests/cli/roundtrip-external-id.wit | 42 ++++++++ tests/cli/roundtrip-external-id.wit.stdout | 38 ++++++++ .../cli/component-model/implements.wast.json | 11 +-- .../component-model/implements.wast/13.print | 31 ++++++ 26 files changed, 476 insertions(+), 151 deletions(-) create mode 100644 tests/cli/dummy-external-id-nested-items.wit create mode 100644 tests/cli/dummy-external-id-nested-items.wit.stdout create mode 100644 tests/cli/dummy-external-id-world-items.wit create mode 100644 tests/cli/dummy-external-id-world-items.wit.stdout delete mode 100644 tests/cli/dummy-external-id2.wit delete mode 100644 tests/cli/dummy-external-id2.wit.stdout create mode 100644 tests/cli/external-id-in-binary-format.wit create mode 100644 tests/cli/external-id-in-binary-format.wit.stdout create mode 100644 tests/cli/roundtrip-external-id.wit create mode 100644 tests/cli/roundtrip-external-id.wit.stdout diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs index a5e9813734..17498444ea 100644 --- a/crates/wasmparser/src/validator/component.rs +++ b/crates/wasmparser/src/validator/component.rs @@ -4725,10 +4725,6 @@ impl ComponentNameContext { "the `cm-implements` feature is not active", offset, )?; - match ty { - ComponentEntityType::Instance(_) => {} - _ => bail!(offset, "only instances can have an `external-id`"), - } } // Validate that the kebab name, if it has structure such as diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs index 83d1f894c7..4377ec5134 100644 --- a/crates/wit-component/src/encoding.rs +++ b/crates/wit-component/src/encoding.rs @@ -549,7 +549,10 @@ impl<'a> EncodingState<'a> { log::trace!("encoding function type for `{}`", func.name); let idx = encoder.encode_func_type(resolve, func)?; - encoder.ty.export(&func.name, ComponentTypeRef::Func(idx)); + encoder.ty.export( + crate::encoding::types::extern_name(&func.name, func.external_id.as_deref()), + ComponentTypeRef::Func(idx), + ); } let ty = encoder.ty; @@ -597,7 +600,10 @@ impl<'a> EncodingState<'a> { let idx = self .root_import_type_encoder(None) .encode_func_type(resolve, func)?; - let func_idx = self.component.import(&name, ComponentTypeRef::Func(idx)); + let func_idx = self.component.import( + crate::encoding::types::extern_name(name.as_str(), func.external_id.as_deref()), + ComponentTypeRef::Func(idx), + ); let prev = self.imported_funcs.insert(name, func_idx); assert!(prev.is_none()); } @@ -735,8 +741,15 @@ impl<'a> EncodingState<'a> { .encode_func_type(resolve, func)?; let core_name = world_func_core_names[&func.name]; let idx = self.encode_lift(module, &core_name, export_name, func, ty)?; - self.component - .export(export_string, ComponentExportKind::Func, idx, None); + self.component.export( + crate::encoding::types::extern_name( + &export_string, + func.external_id.as_deref(), + ), + ComponentExportKind::Func, + idx, + None, + ); } item @ WorldItem::Interface { id, .. } => { let core_names = interface_func_core_names.get(export_name); @@ -913,7 +926,10 @@ impl<'a> EncodingState<'a> { match ty.kind { TypeDefKind::Resource => { let idx = nested.component.export( - ty.name.as_ref().expect("resources must be named"), + crate::encoding::types::extern_name( + ty.name.as_ref().expect("resources must be named"), + ty.external_id.as_deref(), + ), ComponentExportKind::Type, resources[id], None, @@ -929,7 +945,7 @@ impl<'a> EncodingState<'a> { for (i, (_, func)) in resolve.interfaces[export].functions.iter().enumerate() { let ty = nested.encode_func_type(resolve, func)?; nested.component.export( - &func.name, + crate::encoding::types::extern_name(&func.name, func.external_id.as_deref()), ComponentExportKind::Func, i as u32, Some(ComponentTypeRef::Func(ty)), @@ -994,14 +1010,18 @@ impl<'a> EncodingState<'a> { fn define_function_type(&mut self) -> (u32, ComponentFuncTypeEncoder<'_>) { self.component.type_function(None) } - fn export_type(&mut self, idx: u32, name: &'a str) -> Option { + fn export_type( + &mut self, + idx: u32, + name: wasm_encoder::ComponentExternName<'a>, + ) -> Option { if self.export_types { Some( self.component .export(name, ComponentExportKind::Type, idx, None), ) } else { - let name = self.unique_import_name(name); + let name = self.unique_import_name(&name.name); let ret = self .component .import(&name, ComponentTypeRef::Type(TypeBounds::Eq(idx))); @@ -1009,11 +1029,11 @@ impl<'a> EncodingState<'a> { Some(ret) } } - fn export_resource(&mut self, name: &'a str) -> u32 { + fn export_resource(&mut self, name: wasm_encoder::ComponentExternName<'a>) -> u32 { if self.export_types { panic!("resources should already be exported") } else { - let name = self.unique_import_name(name); + let name = self.unique_import_name(&name.name); let ret = self .component .import(&name, ComponentTypeRef::Type(TypeBounds::SubResource)); @@ -2944,6 +2964,7 @@ impl<'a> Shims<'a> { docs: Default::default(), stability: Stability::Unknown, span: Default::default(), + external_id: None, }, if async_ { AbiVariant::GuestImportAsync @@ -3046,6 +3067,7 @@ fn task_return_options_and_type( docs: Default::default(), stability: Stability::Unknown, span: Default::default(), + external_id: None, }; let abi = AbiVariant::GuestImport; let mut options = RequiredOptions::for_import(resolve, func, abi); diff --git a/crates/wit-component/src/encoding/types.rs b/crates/wit-component/src/encoding/types.rs index 2379a47fda..6b43e5a84e 100644 --- a/crates/wit-component/src/encoding/types.rs +++ b/crates/wit-component/src/encoding/types.rs @@ -107,11 +107,11 @@ pub trait ValtypeEncoder<'a> { fn define_function_type(&mut self) -> (u32, ComponentFuncTypeEncoder<'_>); /// Creates an export item for the specified type index. - fn export_type(&mut self, index: u32, name: &'a str) -> Option; + fn export_type(&mut self, index: u32, name: ComponentExternName<'a>) -> Option; /// Creates a new `(type (sub resource))` export with the given name, /// returning the type index that refers to the fresh type created. - fn export_resource(&mut self, name: &'a str) -> u32; + fn export_resource(&mut self, name: ComponentExternName<'a>) -> u32; /// Returns the encoding maps used to encoding types such as id-to-index /// maps. @@ -235,7 +235,8 @@ pub trait ValtypeEncoder<'a> { TypeDefKind::Unknown => unreachable!(), TypeDefKind::Resource => { let name = ty.name.as_ref().expect("resources must be named"); - let index = self.export_resource(name); + let index = + self.export_resource(extern_name(name, ty.external_id.as_deref())); self.type_encoding_maps().id_to_index.insert(id, index); return Ok(ComponentValType::Type(index)); } @@ -270,7 +271,9 @@ pub trait ValtypeEncoder<'a> { index } }; - let index = self.export_type(index, name).unwrap_or(index); + let index = self + .export_type(index, extern_name(name, ty.external_id.as_deref())) + .unwrap_or(index); encoded = ComponentValType::Type(index); } @@ -429,6 +432,17 @@ pub trait ValtypeEncoder<'a> { } } +/// Helper to create a `ComponentExternName` from its component parts found +/// within a WIT AST node. +pub fn extern_name<'a>(name: &'a str, external_id: Option<&'a str>) -> ComponentExternName<'a> { + ComponentExternName { + name: name.into(), + implements: None, + external_id: external_id.map(|s| s.into()), + version_suffix: None, + } +} + pub struct RootTypeEncoder<'state, 'a> { pub state: &'state mut EncodingState<'a>, pub interface: Option, @@ -445,7 +459,7 @@ impl<'a> ValtypeEncoder<'a> for RootTypeEncoder<'_, 'a> { fn interface(&self) -> Option { self.interface } - fn export_type(&mut self, idx: u32, name: &'a str) -> Option { + fn export_type(&mut self, idx: u32, name: ComponentExternName<'a>) -> Option { // When encoding types for the root the root component will export // this type, but when encoding types for a targeted interface then we // can't export types just yet. Interfaces will be created as an @@ -466,7 +480,7 @@ impl<'a> ValtypeEncoder<'a> for RootTypeEncoder<'_, 'a> { None } } - fn export_resource(&mut self, name: &'a str) -> u32 { + fn export_resource(&mut self, name: ComponentExternName<'a>) -> u32 { assert!(self.interface.is_none()); assert!(self.import_types); self.state @@ -495,13 +509,13 @@ impl<'a> ValtypeEncoder<'a> for InstanceTypeEncoder<'_, 'a> { fn define_function_type(&mut self) -> (u32, ComponentFuncTypeEncoder<'_>) { (self.ty.type_count(), self.ty.ty().function()) } - fn export_type(&mut self, idx: u32, name: &str) -> Option { + fn export_type(&mut self, idx: u32, name: ComponentExternName<'a>) -> Option { let ret = self.ty.type_count(); self.ty .export(name, ComponentTypeRef::Type(TypeBounds::Eq(idx))); Some(ret) } - fn export_resource(&mut self, name: &str) -> u32 { + fn export_resource(&mut self, name: ComponentExternName<'a>) -> u32 { let ret = self.ty.type_count(); self.ty .export(name, ComponentTypeRef::Type(TypeBounds::SubResource)); diff --git a/crates/wit-component/src/encoding/wit.rs b/crates/wit-component/src/encoding/wit.rs index 3fb5969fda..a70f90cc38 100644 --- a/crates/wit-component/src/encoding/wit.rs +++ b/crates/wit-component/src/encoding/wit.rs @@ -1,4 +1,4 @@ -use crate::encoding::types::{TypeEncodingMaps, ValtypeEncoder}; +use crate::encoding::types::{TypeEncodingMaps, ValtypeEncoder, extern_name}; use anyhow::Result; use indexmap::IndexSet; use std::collections::HashMap; @@ -295,10 +295,10 @@ impl InterfaceEncoder<'_> { for (name, func) in funcs { let ty = self.encode_func_type(self.resolve, func)?; - self.ty - .as_mut() - .unwrap() - .export(name, ComponentTypeRef::Func(ty)); + self.ty.as_mut().unwrap().export( + extern_name(name, func.external_id.as_deref()), + ComponentTypeRef::Func(ty), + ); } let instance = self.pop_instance(); let idx = self.outer.type_count(); @@ -335,7 +335,7 @@ impl<'a> ValtypeEncoder<'a> for InterfaceEncoder<'a> { None => (self.outer.type_count(), self.outer.ty().function()), } } - fn export_type(&mut self, index: u32, name: &'a str) -> Option { + fn export_type(&mut self, index: u32, name: ComponentExternName<'a>) -> Option { match &mut self.ty { Some(ty) => { assert!(!self.import_types); @@ -356,7 +356,7 @@ impl<'a> ValtypeEncoder<'a> for InterfaceEncoder<'a> { } } } - fn export_resource(&mut self, name: &'a str) -> u32 { + fn export_resource(&mut self, name: ComponentExternName<'a>) -> u32 { let type_ref = ComponentTypeRef::Type(TypeBounds::SubResource); match &mut self.ty { Some(ty) => { diff --git a/crates/wit-component/src/encoding/world.rs b/crates/wit-component/src/encoding/world.rs index fa01a5c391..dfb56b2795 100644 --- a/crates/wit-component/src/encoding/world.rs +++ b/crates/wit-component/src/encoding/world.rs @@ -261,7 +261,14 @@ impl<'a> ComponentWorld<'a> { WorldItem::Interface { id, .. } => Some(*id), }; let implements = resolve.implements_value(key, item); - let external_id = resolve.external_id_value(key, item); + // Note that `external_id` is only tracked for interface imports + // here. World-level functions and types all share the `None` entry + // in `import_map` but each item can have its own `external-id` + // which is emitted on a per-item basis instead. + let external_id = match item { + WorldItem::Function(_) | WorldItem::Type { .. } => None, + WorldItem::Interface { .. } => resolve.external_id_value(key, item), + }; let interface = import_map .entry(import_map_key) .or_insert_with(|| ImportedInterface { diff --git a/crates/wit-component/src/printing.rs b/crates/wit-component/src/printing.rs index d06e4565a6..a9381465a9 100644 --- a/crates/wit-component/src/printing.rs +++ b/crates/wit-component/src/printing.rs @@ -171,6 +171,7 @@ impl WitPrinter { self.new_item(); self.print_docs(&func.docs); self.print_stability(&func.stability); + self.print_external_id(func.external_id.as_deref()); self.print_name_type(func.item_name(), TypeKind::FunctionFreestanding); self.output.str(": "); self.print_function(resolve, func)?; @@ -193,29 +194,40 @@ impl WitPrinter { // Partition types defined in this interface into either those imported // from foreign interfaces or those defined locally. let mut types_to_declare = Vec::new(); - let mut types_to_import: Vec<(_, &_, Vec<_>)> = Vec::new(); + let mut types_to_import: Vec<(_, &TypeDef, Vec<_>)> = Vec::new(); for (name, ty_id) in types { let ty = &resolve.types[ty_id]; + + // If `ty` points to another type, `other`, then this might actually + // be a `use`. if let TypeDefKind::Type(Type::Id(other)) = ty.kind { let other = &resolve.types[other]; match other.owner { TypeOwner::None => {} + + // `use` is only applicable when the owner of the current + // set of types is different than the owner of `other`. Once + // this is detected `types_to_import` is going to get + // modified. other_owner if owner != other_owner => { let other_name = other .name .as_ref() .ok_or_else(|| anyhow!("cannot import unnamed type"))?; - if let Some((owner, stability, list)) = types_to_import.last_mut() { - if *owner == other_owner && ty.stability == **stability { + + // As a convenience push onto the last set of types to + // import if it's to the same interface and with + // matching attributes. + if let Some((prev_owner, prev_ty, list)) = types_to_import.last_mut() { + if *prev_owner == other_owner + && ty.stability == prev_ty.stability + && ty.external_id == prev_ty.external_id + { list.push((name, other_name)); continue; } } - types_to_import.push(( - other_owner, - &ty.stability, - vec![(name, other_name)], - )); + types_to_import.push((other_owner, ty, vec![(name, other_name)])); continue; } _ => {} @@ -231,9 +243,10 @@ impl WitPrinter { TypeOwner::World(id) => resolve.worlds[id].package.unwrap(), TypeOwner::None => unreachable!(), }; - for (owner, stability, tys) in types_to_import { + for (owner, ty, tys) in types_to_import { self.any_items = true; - self.print_stability(stability); + self.print_stability(&ty.stability); + self.print_external_id(ty.external_id.as_deref()); self.output.keyword("use"); self.output.str(" "); let id = match owner { @@ -266,6 +279,7 @@ impl WitPrinter { self.new_item(); self.print_docs(&resolve.types[id].docs); self.print_stability(&resolve.types[id].stability); + self.print_external_id(resolve.types[id].external_id.as_deref()); match resolve.types[id].kind { TypeDefKind::Resource => self.print_resource( resolve, @@ -295,6 +309,7 @@ impl WitPrinter { for func in funcs { self.print_docs(&func.docs); self.print_stability(&func.stability); + self.print_external_id(func.external_id.as_deref()); match &func.kind { FunctionKind::Constructor(_) => {} @@ -457,15 +472,7 @@ impl WitPrinter { // `docs`); for an inline `import x: interface { .. }` with no statement // docs fall back to the interface definition's docs. let docs = match item { - WorldItem::Interface { - id, - docs, - external_id, - .. - } => { - if let Some(id) = external_id { - self.print_external_id(id); - } + WorldItem::Interface { id, docs, .. } => { if docs.contents.is_some() { Some(docs) } else if matches!(name, WorldKey::Name(_)) { @@ -483,6 +490,7 @@ impl WitPrinter { } self.print_stability(item.stability(resolve)); + self.print_external_id(resolve.external_id_value(name, item).as_deref()); self.output.keyword(import_or_export_keyword); self.output.str(" "); match name { @@ -1185,7 +1193,10 @@ impl WitPrinter { } } - fn print_external_id(&mut self, id: &str) { + fn print_external_id(&mut self, id: Option<&str>) { + let Some(id) = id else { + return; + }; self.output.keyword("@external-id"); self.output.str("(\""); let mut buf = [0; 4]; diff --git a/crates/wit-dylib/tests/roundtrip.rs b/crates/wit-dylib/tests/roundtrip.rs index 52691531f3..59a40adc3e 100644 --- a/crates/wit-dylib/tests/roundtrip.rs +++ b/crates/wit-dylib/tests/roundtrip.rs @@ -136,6 +136,7 @@ fn run_one(u: &mut Unstructured<'_>) -> Result<()> { stability: Default::default(), docs: Default::default(), span: Default::default(), + external_id: Default::default(), }, ); funcs.insert( @@ -152,6 +153,7 @@ fn run_one(u: &mut Unstructured<'_>) -> Result<()> { stability: Default::default(), docs: Default::default(), span: Default::default(), + external_id: Default::default(), }, ); funcs.insert( @@ -164,6 +166,7 @@ fn run_one(u: &mut Unstructured<'_>) -> Result<()> { stability: Default::default(), docs: Default::default(), span: Default::default(), + external_id: Default::default(), }, ); funcs @@ -240,6 +243,7 @@ fn run_one(u: &mut Unstructured<'_>) -> Result<()> { stability: Default::default(), docs: Default::default(), span: Default::default(), + external_id: Default::default(), }), ); @@ -343,6 +347,7 @@ fn update_resources(resolve: &mut Resolve) { docs: Default::default(), stability: Default::default(), span: Default::default(), + external_id: Default::default(), }); let borrow = resolve.types.alloc(TypeDef { name: None, @@ -351,6 +356,7 @@ fn update_resources(resolve: &mut Resolve) { docs: Default::default(), stability: Default::default(), span: Default::default(), + external_id: Default::default(), }); let iface = &mut resolve.interfaces[interface_id]; let ctor = format!("[constructor]{resource_name}"); @@ -372,6 +378,7 @@ fn update_resources(resolve: &mut Resolve) { stability: Default::default(), docs: Default::default(), span: Default::default(), + external_id: Default::default(), }, ); iface.functions.insert( @@ -388,6 +395,7 @@ fn update_resources(resolve: &mut Resolve) { stability: Default::default(), docs: Default::default(), span: Default::default(), + external_id: Default::default(), }, ); } diff --git a/crates/wit-parser/src/ast/resolve.rs b/crates/wit-parser/src/ast/resolve.rs index c42f0191df..3ac115a27d 100644 --- a/crates/wit-parser/src/ast/resolve.rs +++ b/crates/wit-parser/src/ast/resolve.rs @@ -560,6 +560,7 @@ impl<'a> Resolver<'a> { None => return Ok(()), }; let stability = self.stability(attrs)?; + let external_id = self.external_id(attrs)?; let (item, name, span) = self.resolve_ast_item_path(path)?; let iface = self.extract_iface_from_item(&item, &name, span)?; if !self.foreign_interfaces.contains(&iface) { @@ -581,6 +582,7 @@ impl<'a> Resolver<'a> { name: Some(name.name.name.to_string()), owner: TypeOwner::Interface(iface), span: name.name.span, + external_id: external_id.clone(), }); self.unknown_type_spans.push(name.name.span); lookup.insert(name.name.name, (TypeOrItem::Type(id), name.name.span)); @@ -945,6 +947,7 @@ impl<'a> Resolver<'a> { }; let docs = self.docs(&def.docs); let stability = self.stability(&def.attributes)?; + let external_id = self.external_id(&def.attributes)?; let kind = self.resolve_type_def(&def.ty, &stability)?; let id = self.types.alloc(TypeDef { docs, @@ -953,6 +956,7 @@ impl<'a> Resolver<'a> { name: Some(def.name.name.to_string()), owner, span: def.name.span, + external_id, }); self.define_interface_name(&def.name, TypeOrItem::Type(id))?; } @@ -980,6 +984,7 @@ impl<'a> Resolver<'a> { let (item, name, span) = self.resolve_ast_item_path(&u.from)?; let use_from = self.extract_iface_from_item(&item, &name, span)?; let stability = self.stability(&u.attributes)?; + let external_id = self.external_id(&u.attributes)?; for name in u.names.iter() { let lookup = &self.interface_types[use_from.index()]; @@ -1009,6 +1014,7 @@ impl<'a> Resolver<'a> { name: Some(name.name.to_string()), owner, span, + external_id: external_id.clone(), }); self.define_interface_name(name, TypeOrItem::Type(id))?; } @@ -1092,6 +1098,7 @@ impl<'a> Resolver<'a> { ) -> ParseResult { let docs = self.docs(docs); let stability = self.stability(attrs)?; + let external_id = self.external_id(attrs)?; let params = self.resolve_params(&func.params, &kind, func.span)?; let result = self.resolve_result(&func.result, &kind, func.span)?; Ok(Function { @@ -1102,6 +1109,7 @@ impl<'a> Resolver<'a> { params, result, span: name_span, + external_id, }) } @@ -1484,6 +1492,7 @@ impl<'a> Resolver<'a> { stability, owner: TypeOwner::None, span: ty.span(), + external_id: None, })) } @@ -1706,6 +1715,7 @@ impl<'a> Resolver<'a> { name: None, owner: TypeOwner::None, span, + external_id: None, }); ret.push(Param { name: "self".to_string(), diff --git a/crates/wit-parser/src/decoding.rs b/crates/wit-parser/src/decoding.rs index 4b2d949345..71ec28edf9 100644 --- a/crates/wit-parser/src/decoding.rs +++ b/crates/wit-parser/src/decoding.rs @@ -11,8 +11,8 @@ use wasmparser::{ ComponentExternalKind, Parser, Payload, PrimitiveValType, ValidPayload, Validator, WasmFeatures, component_types::{ - ComponentAnyTypeId, ComponentDefinedType, ComponentEntityType, ComponentFuncType, - ComponentItem, ComponentType, ComponentValType, + ComponentAnyTypeId, ComponentDefinedType, ComponentEntityType, ComponentItem, + ComponentType, ComponentValType, }, names::{ComponentName, ComponentNameKind}, types, @@ -645,19 +645,15 @@ impl WitPackageDecoder<'_> { ComponentEntityType::Instance(_) => self .decode_world_instance(name, item, package) .with_context(|| format!("failed to decode WIT from import `{name}`"))?, - ComponentEntityType::Func(i) => { - let ty = &self.types[i]; + ComponentEntityType::Func(_) => { let func = self - .convert_function(name, ty, owner) + .convert_function(name, item, owner) .with_context(|| format!("failed to decode function from import `{name}`"))?; (WorldKey::Name(name.to_string()), WorldItem::Function(func)) } - ComponentEntityType::Type { - referenced, - created, - } => { + ComponentEntityType::Type { .. } => { let id = self - .register_type_export(name, owner, referenced, created) + .register_type_export(name, item, owner) .with_context(|| format!("failed to decode type from export `{name}`"))?; ( WorldKey::Name(name.to_string()), @@ -683,10 +679,9 @@ impl WitPackageDecoder<'_> { ) -> Result<()> { log::debug!("decoding component export `{name}`"); let (name, item) = match item.ty { - ComponentEntityType::Func(i) => { - let ty = &self.types[i]; + ComponentEntityType::Func(_) => { let func = self - .convert_function(name, ty, TypeOwner::World(world)) + .convert_function(name, item, TypeOwner::World(world)) .with_context(|| format!("failed to decode function from export `{name}`"))?; (WorldKey::Name(name.to_string()), WorldItem::Function(func)) @@ -760,9 +755,9 @@ impl WitPackageDecoder<'_> { None => (false, self.extract_dep_interface(name, item)?), }; let owner = TypeOwner::Interface(interface); - for (name, ty) in ty.exports.iter() { + for (name, item) in ty.exports.iter() { log::debug!("decoding import instance export `{name}`"); - match ty.ty { + match item.ty { ComponentEntityType::Type { referenced, created, @@ -819,12 +814,7 @@ impl WitPackageDecoder<'_> { if is_local { bail!("instance type export `{name}` not defined in interface"); } - let id = self.register_type_export( - name.as_str(), - owner, - referenced, - created, - )?; + let id = self.register_type_export(name.as_str(), item, owner)?; let prev = self.resolve.interfaces[interface] .types .insert(name.to_string(), id); @@ -836,8 +826,7 @@ impl WitPackageDecoder<'_> { // This has similar logic to types above where we lazily fill in // functions for remote dependencies and otherwise assert // they're already defined for local dependencies. - ComponentEntityType::Func(ty) => { - let def = &self.types[ty]; + ComponentEntityType::Func(_) => { if self.resolve.interfaces[interface] .functions .contains_key(name.as_str()) @@ -849,7 +838,7 @@ impl WitPackageDecoder<'_> { if is_local { bail!("instance function export `{name}` not defined in interface"); } - let func = self.convert_function(name.as_str(), def, owner)?; + let func = self.convert_function(name.as_str(), item, owner)?; let prev = self.resolve.interfaces[interface] .functions .insert(name.to_string(), func); @@ -982,23 +971,19 @@ impl WitPackageDecoder<'_> { }; let owner = TypeOwner::Interface(self.resolve.interfaces.next_id()); - for (name, ty) in ty.exports.iter() { - match ty.ty { - ComponentEntityType::Type { - referenced, - created, - } => { + for (name, item) in ty.exports.iter() { + match item.ty { + ComponentEntityType::Type { .. } => { let ty = self - .register_type_export(name.as_str(), owner, referenced, created) + .register_type_export(name.as_str(), item, owner) .with_context(|| format!("failed to register type export '{name}'"))?; let prev = interface.types.insert(name.to_string(), ty); assert!(prev.is_none()); } - ComponentEntityType::Func(ty) => { - let ty = &self.types[ty]; + ComponentEntityType::Func(_) => { let func = self - .convert_function(name.as_str(), ty, owner) + .convert_function(name.as_str(), item, owner) .with_context(|| format!("failed to convert function '{name}'"))?; let prev = interface.functions.insert(name.to_string(), func); assert!(prev.is_none()); @@ -1043,10 +1028,16 @@ impl WitPackageDecoder<'_> { fn register_type_export( &mut self, name: &str, + item: &ComponentItem, owner: TypeOwner, - referenced: ComponentAnyTypeId, - created: ComponentAnyTypeId, ) -> Result { + let (referenced, created) = match item.ty { + ComponentEntityType::Type { + referenced, + created, + } => (referenced, created), + _ => unreachable!(), + }; let kind = match self.find_alias(referenced) { // If this `TypeId` points to a type which has // previously been defined, meaning we're aliasing a @@ -1076,6 +1067,7 @@ impl WitPackageDecoder<'_> { stability: Default::default(), owner, span: Default::default(), + external_id: item.external_id.clone(), }); // If this is a resource then doubly-register it in `self.resources` so @@ -1125,17 +1117,13 @@ impl WitPackageDecoder<'_> { }; let owner = TypeOwner::World(self.resolve.worlds.next_id()); - for (name, ty) in ty.imports.iter() { - let (name, item) = match ty.ty { + for (name, item) in ty.imports.iter() { + let (name, item) = match item.ty { ComponentEntityType::Instance(_) => { - self.decode_world_instance(name, ty, package)? + self.decode_world_instance(name, item, package)? } - ComponentEntityType::Type { - created, - referenced, - } => { - let ty = - self.register_type_export(name.as_str(), owner, referenced, created)?; + ComponentEntityType::Type { .. } => { + let ty = self.register_type_export(name.as_str(), item, owner)?; ( WorldKey::Name(name.to_string()), WorldItem::Type { @@ -1144,9 +1132,8 @@ impl WitPackageDecoder<'_> { }, ) } - ComponentEntityType::Func(idx) => { - let ty = &self.types[idx]; - let func = self.convert_function(name.as_str(), ty, owner)?; + ComponentEntityType::Func(_) => { + let func = self.convert_function(name.as_str(), item, owner)?; (WorldKey::Name(name.to_string()), WorldItem::Function(func)) } _ => bail!("component import `{name}` is not an instance, func, or type"), @@ -1160,9 +1147,8 @@ impl WitPackageDecoder<'_> { self.decode_world_instance(name, item, package)? } - ComponentEntityType::Func(idx) => { - let ty = &self.types[idx]; - let func = self.convert_function(name.as_str(), ty, owner)?; + ComponentEntityType::Func(_) => { + let func = self.convert_function(name.as_str(), item, owner)?; (WorldKey::Name(name.to_string()), WorldItem::Function(func)) } @@ -1179,9 +1165,13 @@ impl WitPackageDecoder<'_> { fn convert_function( &mut self, name: &str, - ty: &ComponentFuncType, + item: &ComponentItem, owner: TypeOwner, ) -> Result { + let ty = match item.ty { + ComponentEntityType::Func(i) => &self.types[i], + _ => unreachable!(), + }; let name = ComponentName::new(name, 0).unwrap(); let params = ty .params @@ -1205,6 +1195,7 @@ impl WitPackageDecoder<'_> { Ok(Function { docs: Default::default(), stability: Default::default(), + external_id: item.external_id.clone(), kind: match name.kind() { ComponentNameKind::Label(_) => { if ty.async_ { @@ -1296,6 +1287,7 @@ impl WitPackageDecoder<'_> { owner: TypeOwner::None, kind, span: Default::default(), + external_id: None, }); let prev = self.type_map.insert(id.into(), ty); assert!(prev.is_none()); diff --git a/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs index e4c261e318..05e19bfe1f 100644 --- a/crates/wit-parser/src/lib.rs +++ b/crates/wit-parser/src/lib.rs @@ -628,6 +628,8 @@ pub struct TypeDef { /// Source span for this type. #[cfg_attr(feature = "serde", serde(skip))] pub span: Span, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub external_id: Option, } impl TypeDef { @@ -949,6 +951,8 @@ pub struct Function { /// Source span for this function. #[cfg_attr(feature = "serde", serde(skip))] pub span: Span, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub external_id: Option, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -1520,6 +1524,7 @@ mod test { docs: Docs::default(), stability: Stability::Unknown, span: Default::default(), + external_id: Default::default(), }); let t1 = resolve.types.alloc(TypeDef { name: None, @@ -1528,6 +1533,7 @@ mod test { docs: Docs::default(), stability: Stability::Unknown, span: Default::default(), + external_id: Default::default(), }); let t2 = resolve.types.alloc(TypeDef { name: None, @@ -1536,6 +1542,7 @@ mod test { docs: Docs::default(), stability: Stability::Unknown, span: Default::default(), + external_id: Default::default(), }); let found = Function { name: "foo".into(), @@ -1556,6 +1563,7 @@ mod test { docs: Docs::default(), stability: Stability::Unknown, span: Default::default(), + external_id: Default::default(), } .find_futures_and_streams(&resolve); assert_eq!(3, found.len()); diff --git a/crates/wit-parser/src/resolve/mod.rs b/crates/wit-parser/src/resolve/mod.rs index 094c4be7d2..5df3631bf3 100644 --- a/crates/wit-parser/src/resolve/mod.rs +++ b/crates/wit-parser/src/resolve/mod.rs @@ -1564,10 +1564,11 @@ impl Resolve { /// See the component model explainer and 🏷️ for more information on this feature. pub fn external_id_value(&self, key: &WorldKey, item: &WorldItem) -> Option { let _ = key; - if let WorldItem::Interface { external_id, .. } = item { - return external_id.clone(); + match item { + WorldItem::Interface { external_id, .. } => external_id.clone(), + WorldItem::Function(f) => f.external_id.clone(), + WorldItem::Type { id, .. } => self.types[*id].external_id.clone(), } - None } /// Returns the interface that `id` uses a type from, if it uses a type from @@ -3521,6 +3522,7 @@ impl Remap { docs: _, stability: _, span: _, + external_id: _, } => *self.own_handles.entry(id).or_insert(new_id), // Everything not-related to `own` doesn't get its ID @@ -3989,6 +3991,7 @@ impl Remap { docs: Default::default(), stability: Default::default(), span: Default::default(), + external_id: None, }) }); } diff --git a/crates/wit-parser/src/sizealign.rs b/crates/wit-parser/src/sizealign.rs index 9ae1d70f9d..ee3df46b19 100644 --- a/crates/wit-parser/src/sizealign.rs +++ b/crates/wit-parser/src/sizealign.rs @@ -565,6 +565,7 @@ mod test { docs: Default::default(), stability: Default::default(), span: Default::default(), + external_id: Default::default(), }); assert_eq!(elem.size, ArchitectureSize::new(usize::MAX, 0)); assert_eq!( @@ -586,6 +587,7 @@ mod test { docs: Default::default(), stability: Default::default(), span: Default::default(), + external_id: Default::default(), }); obj.fill(&resolve); let my_result = crate::Result_ { @@ -599,6 +601,7 @@ mod test { docs: Default::default(), stability: Default::default(), span: Default::default(), + external_id: Default::default(), }); assert_eq!(elem.size, ArchitectureSize::new(8, 2)); assert_eq!(elem.align, Alignment::Pointer); @@ -629,6 +632,7 @@ mod test { docs: Default::default(), stability: Default::default(), span: Default::default(), + external_id: Default::default(), }); assert_eq!(elem.size, ArchitectureSize::new(8, 2)); assert_eq!(elem.align, Alignment::Bytes(NonZeroUsize::new(8).unwrap())); diff --git a/crates/wit-smith/src/generate.rs b/crates/wit-smith/src/generate.rs index 7a1a3bceb0..aeaa62c9c2 100644 --- a/crates/wit-smith/src/generate.rs +++ b/crates/wit-smith/src/generate.rs @@ -592,6 +592,10 @@ impl<'a> InterfaceGenerator<'a> { part.push_str("\n"); } + if u.arbitrary()? { + part.push_str("@external-id(\"hi\")\n"); + } + match u.arbitrary()? { Generate::Use => { if !self.gen_use(u, &mut part, world_name)? { @@ -691,15 +695,8 @@ impl<'a> InterfaceGenerator<'a> { part.push_str("\n"); } - match kind { - ItemKind::AnonInterface(_) - | ItemKind::Interface(_) - | ItemKind::ImplementsInterface(_) => { - if u.arbitrary()? { - part.push_str("@external-id(\"hi\")\n"); - } - } - ItemKind::Func(_) | ItemKind::Type | ItemKind::Use | ItemKind::Include => {} + if u.arbitrary()? { + part.push_str("@external-id(\"hi\")\n"); } if let Some(dir) = direction { diff --git a/tests/cli/component-model/implements.wast b/tests/cli/component-model/implements.wast index f848102839..9416e7545d 100644 --- a/tests/cli/component-model/implements.wast +++ b/tests/cli/component-model/implements.wast @@ -128,6 +128,21 @@ (instance (export "a" (external-id "") (instance $a)) ) + + ;; works on all types of items, not just instances. + (import "i1" (external-id "") (func)) + (import "i2" (external-id "") (component)) + (import "i3" (external-id "") (type (sub resource))) + (import "i4" (external-id "") (core module)) + + ;; works within instances too + (import "i5" (instance + (export "i1" (external-id "") (func)) + (export "i2" (external-id "") (component)) + (export "i3" (external-id "") (type (sub resource))) + (export "i4" (external-id "") (core module)) + (export "i5" (external-id "") (instance)) + )) ) ;; totally unstructured @@ -145,8 +160,3 @@ (assert_malformed (component quote "(import \"a\" (external-id \"\\ff\") (instance))") "malformed UTF-8 encoding") - -;; only on instances -(assert_invalid - (component (import "a" (external-id "") (func))) - "only instances") diff --git a/tests/cli/dummy-external-id-nested-items.wit b/tests/cli/dummy-external-id-nested-items.wit new file mode 100644 index 0000000000..ea07677f48 --- /dev/null +++ b/tests/cli/dummy-external-id-nested-items.wit @@ -0,0 +1,27 @@ +// RUN: component embed % --dummy-names legacy | \ +// component new | \ +// component wit + +package x:name; + +interface i { + @external-id("nested type") + type t = u32; + + @external-id("nested resource") + resource res { + @external-id("nested constructor") + constructor(); + + @external-id("nested method") + m: func(); + } + + @external-id("nested func") + f: func() -> t; +} + +world w { + import i; + export i; +} diff --git a/tests/cli/dummy-external-id-nested-items.wit.stdout b/tests/cli/dummy-external-id-nested-items.wit.stdout new file mode 100644 index 0000000000..9f8e439815 --- /dev/null +++ b/tests/cli/dummy-external-id-nested-items.wit.stdout @@ -0,0 +1,24 @@ +package root:component; + +world root { + import x:name/i; + + export x:name/i; +} +package x:name { + interface i { + @external-id("nested resource") + resource res { + @external-id("nested constructor") + constructor(); + @external-id("nested method") + m: func(); + } + + @external-id("nested type") + type t = u32; + + @external-id("nested func") + f: func() -> t; + } +} diff --git a/tests/cli/dummy-external-id-world-items.wit b/tests/cli/dummy-external-id-world-items.wit new file mode 100644 index 0000000000..8141479210 --- /dev/null +++ b/tests/cli/dummy-external-id-world-items.wit @@ -0,0 +1,15 @@ +// RUN: component embed % --dummy-names legacy | \ +// component new | \ +// component wit + +package x:name; + +world w { + @external-id("hi") + import name: func() -> s64; + import name2: func(); + + @external-id("bye") + export name3: func(); + export name4: func(); +} diff --git a/tests/cli/dummy-external-id-world-items.wit.stdout b/tests/cli/dummy-external-id-world-items.wit.stdout new file mode 100644 index 0000000000..9ff8f41306 --- /dev/null +++ b/tests/cli/dummy-external-id-world-items.wit.stdout @@ -0,0 +1,11 @@ +package root:component; + +world root { + @external-id("hi") + import name: func() -> s64; + import name2: func(); + + @external-id("bye") + export name3: func(); + export name4: func(); +} diff --git a/tests/cli/dummy-external-id2.wit b/tests/cli/dummy-external-id2.wit deleted file mode 100644 index 64625ea2f4..0000000000 --- a/tests/cli/dummy-external-id2.wit +++ /dev/null @@ -1,12 +0,0 @@ -// RUN: component wit % - -package x:name; - -interface i { - x: func(); -} - -world w { - @external-id("hi \t") - import i; -} diff --git a/tests/cli/dummy-external-id2.wit.stdout b/tests/cli/dummy-external-id2.wit.stdout deleted file mode 100644 index e15a5b6c5d..0000000000 --- a/tests/cli/dummy-external-id2.wit.stdout +++ /dev/null @@ -1,11 +0,0 @@ -/// RUN: component wit % -package x:name; - -interface i { - x: func(); -} - -world w { - @external-id("hi /t") - import i; -} diff --git a/tests/cli/external-id-in-binary-format.wit b/tests/cli/external-id-in-binary-format.wit new file mode 100644 index 0000000000..632bbe4f13 --- /dev/null +++ b/tests/cli/external-id-in-binary-format.wit @@ -0,0 +1,45 @@ +// RUN: component wit % --wasm | component wit + +package a:b; + +interface foo { + @external-id("nested type") + type t = u32; + + @external-id("nested record") + record r { + x: u32, + } + + @external-id("nested func") + f: func() -> t; + + @external-id("nested resource") + resource res { + @external-id("nested constructor") + constructor(); + + @external-id("nested method") + m: func(); + + @external-id("nested static") + s: static func(); + } +} + +world w { + @external-id("world type") + type t2 = u32; + + @external-id("world resource") + resource r2; + + @external-id("world use") + use foo.{t}; + + @external-id("world func") + import f2: func() -> t2; + + @external-id("world interface") + import foo; +} diff --git a/tests/cli/external-id-in-binary-format.wit.stdout b/tests/cli/external-id-in-binary-format.wit.stdout new file mode 100644 index 0000000000..4e6a228e25 --- /dev/null +++ b/tests/cli/external-id-in-binary-format.wit.stdout @@ -0,0 +1,40 @@ +/// RUN: component wit % --wasm | component wit +package a:b; + +interface foo { + @external-id("nested type") + type t = u32; + + @external-id("nested record") + record r { + x: u32, + } + + @external-id("nested resource") + resource res { + @external-id("nested constructor") + constructor(); + @external-id("nested method") + m: func(); + @external-id("nested static") + s: static func(); + } + + @external-id("nested func") + f: func() -> t; +} + +world w { + @external-id("world interface") + import foo; + @external-id("world use") + use foo.{t}; + + @external-id("world type") + type t2 = u32; + + @external-id("world resource") + resource r2; + @external-id("world func") + import f2: func() -> t2; +} diff --git a/tests/cli/roundtrip-external-id.wit b/tests/cli/roundtrip-external-id.wit new file mode 100644 index 0000000000..b57d5b5bb5 --- /dev/null +++ b/tests/cli/roundtrip-external-id.wit @@ -0,0 +1,42 @@ +// RUN: component wit % + +package x:name; + +interface i { + @external-id("i") + type some-type = u32; + + @external-id("i") + x: func(); + + @external-id("i") + resource r { + @external-id("i") + constructor(); + + @external-id("i") + x: func(); + } +} + +world w { + @external-id("hi \t") + import i; + + @external-id("hi") + export i; + + @external-id("hi") + resource r; + + @external-id("hi") + type t = u32; + + @external-id("hi") + export x: func(); + @external-id("hi") + import x: func(); + + @external-id("hi") + use i.{some-type}; +} diff --git a/tests/cli/roundtrip-external-id.wit.stdout b/tests/cli/roundtrip-external-id.wit.stdout new file mode 100644 index 0000000000..cb734aa61d --- /dev/null +++ b/tests/cli/roundtrip-external-id.wit.stdout @@ -0,0 +1,38 @@ +/// RUN: component wit % +package x:name; + +interface i { + @external-id("i") + type some-type = u32; + + @external-id("i") + resource r { + @external-id("i") + constructor(); + @external-id("i") + x: func(); + } + + @external-id("i") + x: func(); +} + +world w { + @external-id("hi /t") + import i; + @external-id("hi") + use i.{some-type}; + + @external-id("hi") + resource r; + + @external-id("hi") + type t = u32; + @external-id("hi") + import x: func(); + + @external-id("hi") + export x: func(); + @external-id("hi") + export i; +} diff --git a/tests/snapshots/cli/component-model/implements.wast.json b/tests/snapshots/cli/component-model/implements.wast.json index 0bfc11f910..8cace6d615 100644 --- a/tests/snapshots/cli/component-model/implements.wast.json +++ b/tests/snapshots/cli/component-model/implements.wast.json @@ -99,23 +99,16 @@ }, { "type": "module", - "line": 134, + "line": 149, "filename": "implements.14.wasm", "module_type": "binary" }, { "type": "assert_malformed", - "line": 146, + "line": 161, "filename": "implements.15.wat", "module_type": "text", "text": "malformed UTF-8 encoding" - }, - { - "type": "assert_invalid", - "line": 151, - "filename": "implements.16.wasm", - "module_type": "binary", - "text": "only instances" } ] } \ No newline at end of file diff --git a/tests/snapshots/cli/component-model/implements.wast/13.print b/tests/snapshots/cli/component-model/implements.wast/13.print index b8988ea588..880e8143d4 100644 --- a/tests/snapshots/cli/component-model/implements.wast/13.print +++ b/tests/snapshots/cli/component-model/implements.wast/13.print @@ -46,4 +46,35 @@ (instance (;1;) (export "a" (external-id "") (instance $a)) ) + (type (;2;) (func)) + (import "i1" (external-id "") (func (;0;) (type 2))) + (type (;3;) + (component) + ) + (import "i2" (external-id "") (component (;1;) (type 3))) + (import "i3" (external-id "") (type (;4;) (sub resource))) + (core type (;0;) + (module) + ) + (import "i4" (external-id "") (core module (;0;) (type 0))) + (type (;5;) + (instance + (type (;0;) (func)) + (export (;0;) "i1" (external-id "") (func (type 0))) + (type (;1;) + (component) + ) + (export (;0;) "i2" (external-id "") (component (type 1))) + (export (;2;) "i3" (external-id "") (type (sub resource))) + (core type (;0;) + (module) + ) + (export (;0;) "i4" (external-id "") (core module (type 0))) + (type (;3;) + (instance) + ) + (export (;0;) "i5" (external-id "") (instance (type 3))) + ) + ) + (import "i5" (instance (;2;) (type 5))) )