Repository: bytecodealliance/wasm-tools
Pull request #1508 (2024-04-19 18:26:48)

WIT: Implement @since and @unstable annotations
This PR is an implementation of https://github.com/WebAssembly/component-model/pull/332 for wasm-tools. This is a pretty big feature for WIT so it's a pretty large PR as well. I've tried to split out separate commits where each commit passes test to more easily digest the changes that are happening here.

At a high level:

* Most items in WIT can now be tagged with `@since` and `@unstable`.
* When tagged with `@unstable` items do not appear in `Resolve` by default
* Features are enabled through the set at `Resolve::features` to control whether unstable items appear
* WIT items always retain their stability annotations throught processing, e.g. in `Resolve`
* Round-tripping a WIT document through the WebAssembly component  encoding preserves stability annotations

The intention is that this will be a building block for WASI's release processes. Namely all APIs will start as `@unstable` and only become ungated with `@since` once they're voted to be at such a phase. `@unstable` APIs will be available to all consumers but only on an opt-in basis which will enable everyone to have proper gates and processes around ensuring that unstable APIs aren't relied on without knowing that things may change.

ydnar: Would this PR update the JSON fixtures in the wit-parser crate?
alexcrichton: It does, yes. You can see examples in the `*.json` files in this PR diff.
yoshuawuyts: I spoke with Alex last week - in order to move this forward we want to at a minimum have a reference of what this would look like when applied to the existing WASI WIT definitions. So I'll be doing that next, and share a link to that back here.
yoshuawuyts: Okay, I ported all of WASI to use `@since`, and added the timezones API using `@unstable` in a PR here: https://github.com/WebAssembly/WASI/pull/604. This all seems to work as expected with `wasm-tools`. I think with that we can consider this implementation to be working as intended, and ready to merge?

Patch:
diff --git a/crates/wit-component/src/dummy.rs b/crates/wit-component/src/dummy.rs
index 5aba44f9..6a3cbb45 100644
--- a/crates/wit-component/src/dummy.rs
+++ b/crates/wit-component/src/dummy.rs
@@ -16,7 +16,7 @@ pub fn dummy_module(resolve: &Resolve, world: WorldId) -> Vec<u8> {
                 push_tys(&mut wat, "result", &sig.results);
                 wat.push_str("))\n");
             }
-            WorldItem::Interface(import) => {
+            WorldItem::Interface { id: import, .. } => {
                 let name = resolve.name_world_key(name);
                 for (_, func) in resolve.interfaces[*import].functions.iter() {
                     let sig = resolve.wasm_signature(AbiVariant::GuestImport, func);
@@ -39,7 +39,7 @@ pub fn dummy_module(resolve: &Resolve, world: WorldId) -> Vec<u8> {
     // Import any resource-related functions for exports.
     for (name, export) in world.exports.iter() {
         let export = match export {
-            WorldItem::Interface(export) => *export,
+            WorldItem::Interface { id, .. } => *id,
             _ => continue,
         };
         let module = format!("[export]{}", resolve.name_world_key(name));
@@ -64,7 +64,7 @@ pub fn dummy_module(resolve: &Resolve, world: WorldId) -> Vec<u8> {
             WorldItem::Function(func) => {
                 push_func(&mut wat, &func.name, resolve, func);
             }
-            WorldItem::Interface(export) => {
+            WorldItem::Interface { id: export, .. } => {
                 let name = resolve.name_world_key(name);
                 for (_, func) in resolve.interfaces[*export].functions.iter() {
                     let name = func.core_export_name(Some(&name));
diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs
index 37107c7b..336266de 100644
--- a/crates/wit-component/src/encoding.rs
+++ b/crates/wit-component/src/encoding.rs
@@ -515,7 +515,7 @@ impl<'a> EncodingState<'a> {
         for (name, item) in resolve.worlds[world].imports.iter() {
             let func = match item {
                 WorldItem::Function(f) => f,
-                WorldItem::Interface(_) | WorldItem::Type(_) => continue,
+                WorldItem::Interface { .. } | WorldItem::Type(_) => continue,
             };
             let name = resolve.name_world_key(name);
             if !info.lowerings.contains_key(&name) {
@@ -751,8 +751,8 @@ impl<'a> EncodingState<'a> {
                     self.component
                         .export(&export_string, ComponentExportKind::Func, idx, None);
                 }
-                WorldItem::Interface(export) => {
-                    self.encode_interface_export(&export_string, module, *export)?;
+                WorldItem::Interface { id, .. } => {
+                    self.encode_interface_export(&export_string, module, *id)?;
                 }
                 WorldItem::Type(_) => unreachable!(),
             }
@@ -2001,7 +2001,7 @@ impl ComponentEncoder {
             .with_context(|| {
                 format!("failed to merge WIT packages of adapter `{name}` into main packages")
             })?
-            .worlds[metadata.world.index()];
+            .map_world(metadata.world, None)?;
         self.metadata
             .resolve
             .merge_worlds(world, self.metadata.world)
diff --git a/crates/wit-component/src/encoding/wit/v1.rs b/crates/wit-component/src/encoding/wit/v1.rs
index 5a0a8c64..ed6c39a3 100644
--- a/crates/wit-component/src/encoding/wit/v1.rs
+++ b/crates/wit-component/src/encoding/wit/v1.rs
@@ -32,10 +32,10 @@ pub fn encode_component(resolve: &Resolve, package: PackageId) -> Result<Compone
     };
     encoder.run()?;
 
-    let package_docs = PackageDocs::extract(resolve, package);
+    let package_metadata = PackageMetadata::extract(resolve, package);
     encoder.component.custom_section(&CustomSection {
-        name: PackageDocs::SECTION_NAME.into(),
-        data: package_docs.encode()?.into(),
+        name: PackageMetadata::SECTION_NAME.into(),
+        data: package_metadata.encode()?.into(),
     });
 
     Ok(encoder.component)
@@ -351,9 +351,9 @@ pub fn encode_world(resolve: &Resolve, world_id: WorldId) -> Result<ComponentTyp
         let name = resolve.name_world_key(name);
         log::trace!("encoding import {name}");
         let ty = match import {
-            WorldItem::Interface(i) => {
-                component.interface = Some(*i);
-                let idx = component.encode_instance(*i)?;
+            WorldItem::Interface { id, .. } => {
+                component.interface = Some(*id);
+                let idx = component.encode_instance(*id)?;
                 ComponentTypeRef::Instance(idx)
             }
             WorldItem::Function(f) => {
@@ -376,9 +376,9 @@ pub fn encode_world(resolve: &Resolve, world_id: WorldId) -> Result<ComponentTyp
         let name = resolve.name_world_key(name);
         log::trace!("encoding export {name}");
         let ty = match export {
-            WorldItem::Interface(i) => {
-                component.interface = Some(*i);
-                let idx = component.encode_instance(*i)?;
+            WorldItem::Interface { id, .. } => {
+                component.interface = Some(*id);
+                let idx = component.encode_instance(*id)?;
                 ComponentTypeRef::Instance(idx)
             }
             WorldItem::Function(f) => {
diff --git a/crates/wit-component/src/encoding/wit/v2.rs b/crates/wit-component/src/encoding/wit/v2.rs
index 06a7f4a9..7024c6cc 100644
--- a/crates/wit-component/src/encoding/wit/v2.rs
+++ b/crates/wit-component/src/encoding/wit/v2.rs
@@ -32,10 +32,10 @@ pub fn encode_component(resolve: &Resolve, package: PackageId) -> Result<Compone
     };
     encoder.run()?;
 
-    let package_docs = PackageDocs::extract(resolve, package);
+    let package_metadata = PackageMetadata::extract(resolve, package);
     encoder.component.custom_section(&CustomSection {
-        name: PackageDocs::SECTION_NAME.into(),
-        data: package_docs.encode()?.into(),
+        name: PackageMetadata::SECTION_NAME.into(),
+        data: package_metadata.encode()?.into(),
     });
 
     Ok(encoder.component)
diff --git a/crates/wit-component/src/encoding/world.rs b/crates/wit-component/src/encoding/world.rs
index 1545decd..f7cb669d 100644
--- a/crates/wit-component/src/encoding/world.rs
+++ b/crates/wit-component/src/encoding/world.rs
@@ -120,7 +120,9 @@ impl<'a> ComponentWorld<'a> {
                     .iter()
                     .all(|name| match &resolve.worlds[world].exports[name] {
                         WorldItem::Function(_) => false,
-                        WorldItem::Interface(id) => resolve.interfaces[*id].functions.is_empty(),
+                        WorldItem::Interface { id, .. } => {
+                            resolve.interfaces[*id].functions.is_empty()
+                        }
                         WorldItem::Type(_) => true,
                     })
             };
@@ -209,7 +211,7 @@ impl<'a> ComponentWorld<'a> {
         for name in required_exports {
             match &resolve.worlds[world].exports[name] {
                 WorldItem::Function(func) => add_func(func, None),
-                WorldItem::Interface(id) => {
+                WorldItem::Interface { id, .. } => {
                     let name = resolve.name_world_key(name);
                     for (_, func) in resolve.interfaces[*id].functions.iter() {
                         add_func(func, Some(&name));
@@ -277,11 +279,11 @@ impl<'a> ComponentWorld<'a> {
             let empty = IndexSet::new();
             let import_map_key = match item {
                 WorldItem::Function(_) | WorldItem::Type(_) => None,
-                WorldItem::Interface(_) => Some(name),
+                WorldItem::Interface { .. } => Some(name),
             };
             let interface_id = match item {
                 WorldItem::Function(_) | WorldItem::Type(_) => None,
-                WorldItem::Interface(id) => Some(*id),
+                WorldItem::Interface { id, .. } => Some(*id),
             };
             let required = required
                 .get(import_map_key.as_deref().unwrap_or(BARE_FUNC_MODULE_NAME))
@@ -300,7 +302,7 @@ impl<'a> ComponentWorld<'a> {
                 WorldItem::Type(ty) => {
                     interface.add_type(required, resolve, *ty);
                 }
-                WorldItem::Interface(id) => {
+                WorldItem::Interface { id, .. } => {
                     for (_name, ty) in resolve.interfaces[*id].types.iter() {
                         interface.add_type(required, resolve, *ty);
                     }
@@ -344,7 +346,7 @@ impl<'a> ComponentWorld<'a> {
         for (name, item) in resolve.worlds[world].exports.iter() {
             log::trace!("add live world export `{}`", resolve.name_world_key(name));
             let id = match item {
-                WorldItem::Interface(id) => id,
+                WorldItem::Interface { id, .. } => id,
                 WorldItem::Function(_) | WorldItem::Type(_) => {
                     live.add_world_item(resolve, item);
                     continue;
@@ -400,7 +402,7 @@ impl<'a> ComponentWorld<'a> {
                     log::trace!("add live function import `{name}`");
                     live.add_func(resolve, func);
                 }
-                WorldItem::Interface(id) => {
+                WorldItem::Interface { id, .. } => {
                     let required = match required.get(name.as_str()) {
                         Some(set) => set,
                         None => continue,
@@ -431,7 +433,7 @@ impl<'a> ComponentWorld<'a> {
         for (_name, item) in exports.iter() {
             let id = match item {
                 WorldItem::Function(_) => continue,
-                WorldItem::Interface(id) => *id,
+                WorldItem::Interface { id, .. } => *id,
                 WorldItem::Type(_) => unreachable!(),
             };
             let mut set = HashSet::new();
diff --git a/crates/wit-component/src/metadata.rs b/crates/wit-component/src/metadata.rs
index 5b3286d3..0fdc7e5d 100644
--- a/crates/wit-component/src/metadata.rs
+++ b/crates/wit-component/src/metadata.rs
@@ -92,6 +92,7 @@ impl Default for Bindgen {
             includes: Default::default(),
             include_names: Default::default(),
             package: Some(package),
+            stability: Default::default(),
         });
         resolve.packages[package]
             .worlds
@@ -306,7 +307,7 @@ impl Bindgen {
             .resolve
             .merge(resolve)
             .context("failed to merge WIT package sets together")?
-            .worlds[world.index()];
+            .map_world(world, None)?;
         self.resolve
             .merge_worlds(world, self.world)
             .context("failed to merge worlds from two documents")?;
@@ -361,8 +362,8 @@ impl ModuleMetadata {
                         .insert((BARE_FUNC_MODULE_NAME.to_string(), name.clone()), encoding);
                     assert!(prev.is_none());
                 }
-                WorldItem::Interface(i) => {
-                    for (func, _) in resolve.interfaces[*i].functions.iter() {
+                WorldItem::Interface { id, .. } => {
+                    for (func, _) in resolve.interfaces[*id].functions.iter() {
                         let prev = ret
                             .import_encodings
                             .insert((name.clone(), func.clone()), encoding);
@@ -381,8 +382,8 @@ impl ModuleMetadata {
                     let prev = ret.export_encodings.insert(name.clone(), encoding);
                     assert!(prev.is_none());
                 }
-                WorldItem::Interface(i) => {
-                    for (_, func) in resolve.interfaces[*i].functions.iter() {
+                WorldItem::Interface { id, .. } => {
+                    for (_, func) in resolve.interfaces[*id].functions.iter() {
                         let name = func.core_export_name(Some(&name)).into_owned();
                         let prev = ret.export_encodings.insert(name, encoding);
                         assert!(prev.is_none());
diff --git a/crates/wit-component/src/printing.rs b/crates/wit-component/src/printing.rs
index f5be31c1..33be432d 100644
--- a/crates/wit-component/src/printing.rs
+++ b/crates/wit-component/src/printing.rs
@@ -65,6 +65,7 @@ impl WitPrinter {
         self.output.push_str("\n\n");
         for (name, id) in pkg.interfaces.iter() {
             self.print_docs(&resolve.interfaces[*id].docs);
+            self.print_stability(&resolve.interfaces[*id].stability);
             self.output.push_str("interface ");
             self.print_name(name);
             self.output.push_str(" {\n");
@@ -74,6 +75,7 @@ impl WitPrinter {
 
         for (name, id) in pkg.worlds.iter() {
             self.print_docs(&resolve.worlds[*id].docs);
+            self.print_stability(&resolve.worlds[*id].stability);
             self.output.push_str("world ");
             self.print_name(name);
             self.output.push_str(" {\n");
@@ -125,6 +127,7 @@ impl WitPrinter {
         for (name, func) in freestanding {
             self.new_item();
             self.print_docs(&func.docs);
+            self.print_stability(&func.stability);
             self.print_name(name);
             self.output.push_str(": ");
             self.print_function(resolve, func)?;
@@ -147,7 +150,7 @@ 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<(_, &_, Vec<_>)> = Vec::new();
         for (name, ty_id) in types {
             let ty = &resolve.types[ty_id];
             if let TypeDefKind::Type(Type::Id(other)) = ty.kind {
@@ -159,13 +162,17 @@ impl WitPrinter {
                             .name
                             .as_ref()
                             .ok_or_else(|| anyhow!("cannot import unnamed type"))?;
-                        if let Some((owner, list)) = types_to_import.last_mut() {
-                            if *owner == other_owner {
+                        if let Some((owner, stability, list)) = types_to_import.last_mut() {
+                            if *owner == other_owner && ty.stability == **stability {
                                 list.push((name, other_name));
                                 continue;
                             }
                         }
-                        types_to_import.push((other_owner, vec![(name, other_name)]));
+                        types_to_import.push((
+                            other_owner,
+                            &ty.stability,
+                            vec![(name, other_name)],
+                        ));
                         continue;
                     }
                     _ => {}
@@ -181,8 +188,9 @@ impl WitPrinter {
             TypeOwner::World(id) => resolve.worlds[id].package.unwrap(),
             TypeOwner::None => unreachable!(),
         };
-        for (owner, tys) in types_to_import {
+        for (owner, stability, tys) in types_to_import {
             self.any_items = true;
+            self.print_stability(stability);
             write!(&mut self.output, "use ")?;
             let id = match owner {
                 TypeOwner::Interface(id) => id,
@@ -212,6 +220,7 @@ impl WitPrinter {
         for id in types_to_declare {
             self.new_item();
             self.print_docs(&resolve.types[id].docs);
+            self.print_stability(&resolve.types[id].stability);
             match resolve.types[id].kind {
                 TypeDefKind::Resource => self.print_resource(
                     resolve,
@@ -236,17 +245,16 @@ impl WitPrinter {
         }
         self.output.push_str(" {\n");
         for func in funcs {
+            self.print_docs(&func.docs);
+            self.print_stability(&func.stability);
+
             match &func.kind {
-                FunctionKind::Constructor(_) => {
-                    self.print_docs(&func.docs);
-                }
+                FunctionKind::Constructor(_) => {}
                 FunctionKind::Method(_) => {
-                    self.print_docs(&func.docs);
                     self.print_name(func.item_name());
                     self.output.push_str(": ");
                 }
                 FunctionKind::Static(_) => {
-                    self.print_docs(&func.docs);
                     self.print_name(func.item_name());
                     self.output.push_str(": ");
                     self.output.push_str("static ");
@@ -367,13 +375,14 @@ impl WitPrinter {
         // Print inline item docs
         if matches!(name, WorldKey::Name(_)) {
             self.print_docs(match item {
-                WorldItem::Interface(id) => &resolve.interfaces[*id].docs,
+                WorldItem::Interface { id, .. } => &resolve.interfaces[*id].docs,
                 WorldItem::Function(f) => &f.docs,
                 // Types are handled separately
                 WorldItem::Type(_) => unreachable!(),
             });
         }
 
+        self.print_stability(item.stability(resolve));
         self.output.push_str(desc);
         self.output.push_str(" ");
         match name {
@@ -381,7 +390,7 @@ impl WitPrinter {
                 self.print_name(name);
                 self.output.push_str(": ");
                 match item {
-                    WorldItem::Interface(id) => {
+                    WorldItem::Interface { id, .. } => {
                         assert!(resolve.interfaces[*id].name.is_none());
                         writeln!(self.output, "interface {{")?;
                         self.print_interface(resolve, *id)?;
@@ -398,7 +407,7 @@ impl WitPrinter {
             }
             WorldKey::Interface(id) => {
                 match item {
-                    WorldItem::Interface(id2) => assert_eq!(id, id2),
+                    WorldItem::Interface { id: id2, .. } => assert_eq!(id, id2),
                     _ => unreachable!(),
                 }
                 self.print_path_to_interface(resolve, *id, cur_pkg)?;
@@ -868,6 +877,26 @@ impl WitPrinter {
             }
         }
     }
+
+    fn print_stability(&mut self, stability: &Stability) {
+        match stability {
+            Stability::Unknown => {}
+            Stability::Stable { since, feature } => {
+                self.output.push_str("@since(version = ");
+                self.output.push_str(&since.to_string());
+                if let Some(feature) = feature {
+                    self.output.push_str(", feature = ");
+                    self.output.push_str(feature);
+                }
+                self.output.push_str(")\n");
+            }
+            Stability::Unstable { feature } => {
+                self.output.push_str("@unstable(feature = ");
+                self.output.push_str(feature);
+                self.output.push_str(")\n");
+            }
+        }
+    }
 }
 
 fn resource_func(f: &Function) -> Option<TypeId> {
diff --git a/crates/wit-component/src/validation.rs b/crates/wit-component/src/validation.rs
index 4ce35f9f..fadd430c 100644
--- a/crates/wit-component/src/validation.rs
+++ b/crates/wit-component/src/validation.rs
@@ -237,7 +237,7 @@ pub fn validate_module<'a>(
             }
         } else {
             match world.imports.get(&world_key(&metadata.resolve, name)) {
-                Some(WorldItem::Interface(interface)) => {
+                Some(WorldItem::Interface { id: interface, .. }) => {
                     let required = validate_imported_interface(
                         &metadata.resolve,
                         *interface,
@@ -272,10 +272,10 @@ pub fn validate_module<'a>(
     for (name, interface_name, funcs) in exported_resource_funcs {
         let world_key = world_key(&metadata.resolve, interface_name);
         match world.exports.get(&world_key) {
-            Some(WorldItem::Interface(i)) => {
+            Some(WorldItem::Interface { id, .. }) => {
                 validate_exported_interface_resource_imports(
                     &metadata.resolve,
-                    *i,
+                    *id,
                     name,
                     funcs,
                     &types,
@@ -526,7 +526,7 @@ pub fn validate_adapter_module<'a>(
 
         if !(is_library && adapters.contains(name)) {
             match resolve.worlds[world].imports.get(&world_key(resolve, name)) {
-                Some(WorldItem::Interface(interface)) => {
+                Some(WorldItem::Interface { id: interface, .. }) => {
                     let required =
                         validate_imported_interface(resolve, *interface, name, funcs, &types)
                             .with_context(|| {
@@ -576,10 +576,10 @@ pub fn validate_adapter_module<'a>(
     for (name, interface_name, funcs) in exported_resource_funcs {
         let world_key = world_key(resolve, interface_name);
         match world.exports.get(&world_key) {
-            Some(WorldItem::Interface(i)) => {
+            Some(WorldItem::Interface { id, .. }) => {
                 validate_exported_interface_resource_imports(
                     resolve,
-                    *i,
+                    *id,
                     name,
                     funcs,
                     &types,
@@ -809,7 +809,7 @@ fn validate_exported_item<'a>(
     };
     match item {
         WorldItem::Function(func) => validate(func, None)?,
-        WorldItem::Interface(interface) => {
+        WorldItem::Interface { id: interface, .. } => {
             let interface = &resolve.interfaces[*interface];
             for (_, f) in interface.functions.iter() {
                 validate(f, Some(export_name)).with_context(|| {
diff --git a/crates/wit-parser/src/ast.rs b/crates/wit-parser/src/ast.rs
index 835b39aa..05bd50cf 100644
--- a/crates/wit-parser/src/ast.rs
+++ b/crates/wit-parser/src/ast.rs
@@ -127,10 +127,13 @@ enum AstItem<'a> {
 
 impl<'a> AstItem<'a> {
     fn parse(tokens: &mut Tokenizer<'a>, docs: Docs<'a>) -> Result<Self> {
+        let attributes = Attribute::parse_list(tokens)?;
         match tokens.clone().next()? {
-            Some((_span, Token::Interface)) => Interface::parse(tokens, docs).map(Self::Interface),
-            Some((_span, Token::World)) => World::parse(tokens, docs).map(Self::World),
-            Some((_span, Token::Use)) => ToplevelUse::parse(tokens).map(Self::Use),
+            Some((_span, Token::Interface)) => {
+                Interface::parse(tokens, docs, attributes).map(Self::Interface)
+            }
+            Some((_span, Token::World)) => World::parse(tokens, docs, attributes).map(Self::World),
+            Some((_span, Token::Use)) => ToplevelUse::parse(tokens, attributes).map(Self::Use),
             other => Err(err_expected(tokens, "`world`, `interface` or `use`", other).into()),
         }
     }
@@ -176,13 +179,15 @@ impl<'a> PackageName<'a> {
 }
 
 struct ToplevelUse<'a> {
+    span: Span,
+    attributes: Vec<Attribute<'a>>,
     item: UsePath<'a>,
     as_: Option<Id<'a>>,
 }
 
 impl<'a> ToplevelUse<'a> {
-    fn parse(tokens: &mut Tokenizer<'a>) -> Result<Self> {
-        tokens.expect(Token::Use)?;
+    fn parse(tokens: &mut Tokenizer<'a>, attributes: Vec<Attribute<'a>>) -> Result<Self> {
+        let span = tokens.expect(Token::Use)?;
         let item = UsePath::parse(tokens)?;
         let as_ = if tokens.eat(Token::As)? {
             Some(parse_id(tokens)?)
@@ -190,22 +195,37 @@ impl<'a> ToplevelUse<'a> {
             None
         };
         tokens.expect_semicolon()?;
-        Ok(ToplevelUse { item, as_ })
+        Ok(ToplevelUse {
+            span,
+            attributes,
+            item,
+            as_,
+        })
     }
 }
 
 struct World<'a> {
     docs: Docs<'a>,
+    attributes: Vec<Attribute<'a>>,
     name: Id<'a>,
     items: Vec<WorldItem<'a>>,
 }
 
 impl<'a> World<'a> {
-    fn parse(tokens: &mut Tokenizer<'a>, docs: Docs<'a>) -> Result<Self> {
+    fn parse(
+        tokens: &mut Tokenizer<'a>,
+        docs: Docs<'a>,
+        attributes: Vec<Attribute<'a>>,
+    ) -> Result<Self> {
         tokens.expect(Token::World)?;
         let name = parse_id(tokens)?;
         let items = Self::parse_items(tokens)?;
-        Ok(World { docs, name, items })
+        Ok(World {
+            docs,
+            attributes,
+            name,
+            items,
+        })
     }
 
     fn parse_items(tokens: &mut Tokenizer<'a>) -> Result<Vec<WorldItem<'a>>> {
@@ -216,7 +236,8 @@ impl<'a> World<'a> {
             if tokens.eat(Token::RightBrace)? {
                 break;
             }
-            items.push(WorldItem::parse(tokens, docs)?);
+            let attributes = Attribute::parse_list(tokens)?;
+            items.push(WorldItem::parse(tokens, docs, attributes)?);
         }
         Ok(items)
     }
@@ -231,24 +252,40 @@ enum WorldItem<'a> {
 }
 
 impl<'a> WorldItem<'a> {
-    fn parse(tokens: &mut Tokenizer<'a>, docs: Docs<'a>) -> Result<WorldItem<'a>> {
+    fn parse(
+        tokens: &mut Tokenizer<'a>,
+        docs: Docs<'a>,
+        attributes: Vec<Attribute<'a>>,
+    ) -> Result<WorldItem<'a>> {
         match tokens.clone().next()? {
-            Some((_span, Token::Import)) => Import::parse(tokens, docs).map(WorldItem::Import),
-            Some((_span, Token::Export)) => Export::parse(tokens, docs).map(WorldItem::Export),
-            Some((_span, Token::Use)) => Use::parse(tokens).map(WorldItem::Use),
-            Some((_span, Token::Type)) => TypeDef::parse(tokens, docs).map(WorldItem::Type),
-            Some((_span, Token::Flags)) => TypeDef::parse_flags(tokens, docs).map(WorldItem::Type),
+            Some((_span, Token::Import)) => {
+                Import::parse(tokens, docs, attributes).map(WorldItem::Import)
+            }
+            Some((_span, Token::Export)) => {
+                Export::parse(tokens, docs, attributes).map(WorldItem::Export)
+            }
+            Some((_span, Token::Use)) => Use::parse(tokens, attributes).map(WorldItem::Use),
+            Some((_span, Token::Type)) => {
+                TypeDef::parse(tokens, docs, attributes).map(WorldItem::Type)
+            }
+            Some((_span, Token::Flags)) => {
+                TypeDef::parse_flags(tokens, docs, attributes).map(WorldItem::Type)
+            }
             Some((_span, Token::Resource)) => {
-                TypeDef::parse_resource(tokens, docs).map(WorldItem::Type)
+                TypeDef::parse_resource(tokens, docs, attributes).map(WorldItem::Type)
             }
             Some((_span, Token::Record)) => {
-                TypeDef::parse_record(tokens, docs).map(WorldItem::Type)
+                TypeDef::parse_record(tokens, docs, attributes).map(WorldItem::Type)
             }
             Some((_span, Token::Variant)) => {
-                TypeDef::parse_variant(tokens, docs).map(WorldItem::Type)
+                TypeDef::parse_variant(tokens, docs, attributes).map(WorldItem::Type)
+            }
+            Some((_span, Token::Enum)) => {
+                TypeDef::parse_enum(tokens, docs, attributes).map(WorldItem::Type)
+            }
+            Some((_span, Token::Include)) => {
+                Include::parse(tokens, attributes).map(WorldItem::Include)
             }
-            Some((_span, Token::Enum)) => TypeDef::parse_enum(tokens, docs).map(WorldItem::Type),
-            Some((_span, Token::Include)) => Include::parse(tokens).map(WorldItem::Include),
             other => Err(err_expected(
                 tokens,
                 "`import`, `export`, `include`, `use`, or type definition",
@@ -261,27 +298,45 @@ impl<'a> WorldItem<'a> {
 
 struct Import<'a> {
     docs: Docs<'a>,
+    attributes: Vec<Attribute<'a>>,
     kind: ExternKind<'a>,
 }
 
 impl<'a> Import<'a> {
-    fn parse(tokens: &mut Tokenizer<'a>, docs: Docs<'a>) -> Result<Import<'a>> {
+    fn parse(
+        tokens: &mut Tokenizer<'a>,
+        docs: Docs<'a>,
+        attributes: Vec<Attribute<'a>>,
+    ) -> Result<Import<'a>> {
         tokens.expect(Token::Import)?;
         let kind = ExternKind::parse(tokens)?;
-        Ok(Import { docs, kind })
+        Ok(Import {
+            docs,
+            attributes,
+            kind,
+        })
     }
 }
 
 struct Export<'a> {
     docs: Docs<'a>,
+    attributes: Vec<Attribute<'a>>,
     kind: ExternKind<'a>,
 }
 
 impl<'a> Export<'a> {
-    fn parse(tokens: &mut Tokenizer<'a>, docs: Docs<'a>) -> Result<Export<'a>> {
+    fn parse(
+        tokens: &mut Tokenizer<'a>,
+        docs: Docs<'a>,
+        attributes: Vec<Attribute<'a>>,
+    ) -> Result<Export<'a>> {
         tokens.expect(Token::Export)?;
         let kind = ExternKind::parse(tokens)?;
-        Ok(Export { docs, kind })
+        Ok(Export {
+            docs,
+            attributes,
+            kind,
+        })
     }
 }
 
@@ -339,16 +394,26 @@ impl<'a> ExternKind<'a> {
 
 struct Interface<'a> {
     docs: Docs<'a>,
+    attributes: Vec<Attribute<'a>>,
     name: Id<'a>,
     items: Vec<InterfaceItem<'a>>,
 }
 
 impl<'a> Interface<'a> {
-    fn parse(tokens: &mut Tokenizer<'a>, docs: Docs<'a>) -> Result<Self> {
+    fn parse(
+        tokens: &mut Tokenizer<'a>,
+        docs: Docs<'a>,
+        attributes: Vec<Attribute<'a>>,
+    ) -> Result<Self> {
         tokens.expect(Token::Interface)?;
         let name = parse_id(tokens)?;
         let items = Self::parse_items(tokens)?;
-        Ok(Interface { docs, name, items })
+        Ok(Interface {
+            docs,
+            attributes,
+            name,
+            items,
+        })
     }
 
     pub(super) fn parse_items(tokens: &mut Tokenizer<'a>) -> Result<Vec<InterfaceItem<'a>>> {
@@ -359,7 +424,8 @@ impl<'a> Interface<'a> {
             if tokens.eat(Token::RightBrace)? {
                 break;
             }
-            items.push(InterfaceItem::parse(tokens, docs)?);
+            let attributes = Attribute::parse_list(tokens)?;
+            items.push(InterfaceItem::parse(tokens, docs, attributes)?);
         }
         Ok(items)
     }
@@ -379,6 +445,7 @@ enum InterfaceItem<'a> {
 }
 
 struct Use<'a> {
+    attributes: Vec<Attribute<'a>>,
     from: UsePath<'a>,
     names: Vec<UseName<'a>>,
 }
@@ -432,7 +499,7 @@ struct UseName<'a> {
 }
 
 impl<'a> Use<'a> {
-    fn parse(tokens: &mut Tokenizer<'a>) -> Result<Self> {
+    fn parse(tokens: &mut Tokenizer<'a>, attributes: Vec<Attribute<'a>>) -> Result<Self> {
         tokens.expect(Token::Use)?;
         let from = UsePath::parse(tokens)?;
         tokens.expect(Token::Period)?;
@@ -454,12 +521,17 @@ impl<'a> Use<'a> {
             }
         }
         tokens.expect_semicolon()?;
-        Ok(Use { from, names })
+        Ok(Use {
+            attributes,
+            from,
+            names,
+        })
     }
 }
 
 struct Include<'a> {
     from: UsePath<'a>,
+    attributes: Vec<Attribute<'a>>,
     names: Vec<IncludeName<'a>>,
 }
 
@@ -469,7 +541,7 @@ struct IncludeName<'a> {
 }
 
 impl<'a> Include<'a> {
-    fn parse(tokens: &mut Tokenizer<'a>) -> Result<Self> {
+    fn parse(tokens: &mut Tokenizer<'a>, attributes: Vec<Attribute<'a>>) -> Result<Self> {
         tokens.expect(Token::Include)?;
         let from = UsePath::parse(tokens)?;
 
@@ -490,7 +562,11 @@ impl<'a> Include<'a> {
             Vec::new()
         };
 
-        Ok(Include { from, names })
+        Ok(Include {
+            attributes,
+            from,
+            names,
+        })
     }
 }
 
@@ -526,36 +602,37 @@ impl<'a> Default for Docs<'a> {
 
 struct TypeDef<'a> {
     docs: Docs<'a>,
+    attributes: Vec<Attribute<'a>>,
     name: Id<'a>,
     ty: Type<'a>,
 }
 
 enum Type<'a> {
-    Bool,
-    U8,
-    U16,
-    U32,
-    U64,
-    S8,
-    S16,
-    S32,
-    S64,
-    F32,
-    F64,
-    Char,
-    String,
+    Bool(Span),
+    U8(Span),
+    U16(Span),
+    U32(Span),
+    U64(Span),
+    S8(Span),
+    S16(Span),
+    S32(Span),
+    S64(Span),
+    F32(Span),
+    F64(Span),
+    Char(Span),
+    String(Span),
     Name(Id<'a>),
-    List(Box<Type<'a>>),
+    List(List<'a>),
     Handle(Handle<'a>),
     Resource(Resource<'a>),
     Record(Record<'a>),
     Flags(Flags<'a>),
     Variant(Variant<'a>),
-    Tuple(Vec<Type<'a>>),
+    Tuple(Tuple<'a>),
     Enum(Enum<'a>),
-    Option(Box<Type<'a>>),
+    Option(Option_<'a>),
     Result(Result_<'a>),
-    Future(Option<Box<Type<'a>>>),
+    Future(Future<'a>),
     Stream(Stream<'a>),
 }
 
@@ -564,7 +641,16 @@ enum Handle<'a> {
     Borrow { resource: Id<'a> },
 }
 
+impl Handle<'_> {
+    fn span(&self) -> Span {
+        match self {
+            Handle::Own { resource } | Handle::Borrow { resource } => resource.span,
+        }
+    }
+}
+
 struct Resource<'a> {
+    span: Span,
     funcs: Vec<ResourceFunc<'a>>,
 }
 
@@ -575,7 +661,11 @@ enum ResourceFunc<'a> {
 }
 
 impl<'a> ResourceFunc<'a> {
-    fn parse(docs: Docs<'a>, tokens: &mut Tokenizer<'a>) -> Result<Self> {
+    fn parse(
+        docs: Docs<'a>,
+        attributes: Vec<Attribute<'a>>,
+        tokens: &mut Tokenizer<'a>,
+    ) -> Result<Self> {
         match tokens.clone().next()? {
             Some((span, Token::Constructor)) => {
                 tokens.expect(Token::Constructor)?;
@@ -589,11 +679,13 @@ impl<'a> ResourceFunc<'a> {
                 tokens.expect_semicolon()?;
                 Ok(ResourceFunc::Constructor(NamedFunc {
                     docs,
+                    attributes,
                     name: Id {
                         span,
                         name: "constructor",
                     },
                     func: Func {
+                        span,
                         params,
                         results: ResultList::Named(Vec::new()),
                     },
@@ -609,7 +701,12 @@ impl<'a> ResourceFunc<'a> {
                 };
                 let func = Func::parse(tokens)?;
                 tokens.expect_semicolon()?;
-                Ok(ctor(NamedFunc { docs, name, func }))
+                Ok(ctor(NamedFunc {
+                    docs,
+                    attributes,
+                    name,
+                    func,
+                }))
             }
             other => Err(err_expected(tokens, "`constructor` or identifier", other).into()),
         }
@@ -624,6 +721,7 @@ impl<'a> ResourceFunc<'a> {
 }
 
 struct Record<'a> {
+    span: Span,
     fields: Vec<Field<'a>>,
 }
 
@@ -634,6 +732,7 @@ struct Field<'a> {
 }
 
 struct Flags<'a> {
+    span: Span,
     flags: Vec<Flag<'a>>,
 }
 
@@ -663,18 +762,41 @@ struct EnumCase<'a> {
     name: Id<'a>,
 }
 
+struct Option_<'a> {
+    span: Span,
+    ty: Box<Type<'a>>,
+}
+
+struct List<'a> {
+    span: Span,
+    ty: Box<Type<'a>>,
+}
+
+struct Future<'a> {
+    span: Span,
+    ty: Option<Box<Type<'a>>>,
+}
+
+struct Tuple<'a> {
+    span: Span,
+    types: Vec<Type<'a>>,
+}
+
 struct Result_<'a> {
+    span: Span,
     ok: Option<Box<Type<'a>>>,
     err: Option<Box<Type<'a>>>,
 }
 
 struct Stream<'a> {
+    span: Span,
     element: Option<Box<Type<'a>>>,
     end: Option<Box<Type<'a>>>,
 }
 
 struct NamedFunc<'a> {
     docs: Docs<'a>,
+    attributes: Vec<Attribute<'a>>,
     name: Id<'a>,
     func: Func<'a>,
 }
@@ -687,6 +809,7 @@ enum ResultList<'a> {
 }
 
 struct Func<'a> {
+    span: Span,
     params: ParamList<'a>,
     results: ResultList<'a>,
 }
@@ -705,7 +828,7 @@ impl<'a> Func<'a> {
             })
         }
 
-        tokens.expect(Token::Func)?;
+        let span = tokens.expect(Token::Func)?;
         let params = parse_params(tokens, true)?;
         let results = if tokens.eat(Token::RArrow)? {
             // If we eat a '(', parse the remainder of the named
@@ -720,52 +843,76 @@ impl<'a> Func<'a> {
         } else {
             ResultList::Named(Vec::new())
         };
-        Ok(Func { params, results })
+        Ok(Func {
+            span,
+            params,
+            results,
+        })
     }
 }
 
 impl<'a> InterfaceItem<'a> {
-    fn parse(tokens: &mut Tokenizer<'a>, docs: Docs<'a>) -> Result<InterfaceItem<'a>> {
+    fn parse(
+        tokens: &mut Tokenizer<'a>,
+        docs: Docs<'a>,
+        attributes: Vec<Attribute<'a>>,
+    ) -> Result<InterfaceItem<'a>> {
         match tokens.clone().next()? {
-            Some((_span, Token::Type)) => TypeDef::parse(tokens, docs).map(InterfaceItem::TypeDef),
+            Some((_span, Token::Type)) => {
+                TypeDef::parse(tokens, docs, attributes).map(InterfaceItem::TypeDef)
+            }
             Some((_span, Token::Flags)) => {
-                TypeDef::parse_flags(tokens, docs).map(InterfaceItem::TypeDef)
+                TypeDef::parse_flags(tokens, docs, attributes).map(InterfaceItem::TypeDef)
             }
             Some((_span, Token::Enum)) => {
-                TypeDef::parse_enum(tokens, docs).map(InterfaceItem::TypeDef)
+                TypeDef::parse_enum(tokens, docs, attributes).map(InterfaceItem::TypeDef)
             }
             Some((_span, Token::Variant)) => {
-                TypeDef::parse_variant(tokens, docs).map(InterfaceItem::TypeDef)
+                TypeDef::parse_variant(tokens, docs, attributes).map(InterfaceItem::TypeDef)
             }
             Some((_span, Token::Resource)) => {
-                TypeDef::parse_resource(tokens, docs).map(InterfaceItem::TypeDef)
+                TypeDef::parse_resource(tokens, docs, attributes).map(InterfaceItem::TypeDef)
             }
             Some((_span, Token::Record)) => {
-                TypeDef::parse_record(tokens, docs).map(InterfaceItem::TypeDef)
+                TypeDef::parse_record(tokens, docs, attributes).map(InterfaceItem::TypeDef)
             }
             Some((_span, Token::Id)) | Some((_span, Token::ExplicitId)) => {
-                NamedFunc::parse(tokens, docs).map(InterfaceItem::Func)
+                NamedFunc::parse(tokens, docs, attributes).map(InterfaceItem::Func)
             }
-            Some((_span, Token::Use)) => Use::parse(tokens).map(InterfaceItem::Use),
+            Some((_span, Token::Use)) => Use::parse(tokens, attributes).map(InterfaceItem::Use),
             other => Err(err_expected(tokens, "`type`, `resource` or `func`", other).into()),
         }
     }
 }
 
 impl<'a> TypeDef<'a> {
-    fn parse(tokens: &mut Tokenizer<'a>, docs: Docs<'a>) -> Result<Self> {
+    fn parse(
+        tokens: &mut Tokenizer<'a>,
+        docs: Docs<'a>,
+        attributes: Vec<Attribute<'a>>,
+    ) -> Result<Self> {
         tokens.expect(Token::Type)?;
         let name = parse_id(tokens)?;
         tokens.expect(Token::Equals)?;
         let ty = Type::parse(tokens)?;
         tokens.expect_semicolon()?;
-        Ok(TypeDef { docs, name, ty })
+        Ok(TypeDef {
+            docs,
+            attributes,
+            name,
+            ty,
+        })
     }
 
-    fn parse_flags(tokens: &mut Tokenizer<'a>, docs: Docs<'a>) -> Result<Self> {
+    fn parse_flags(
+        tokens: &mut Tokenizer<'a>,
+        docs: Docs<'a>,
+        attributes: Vec<Attribute<'a>>,
+    ) -> Result<Self> {
         tokens.expect(Token::Flags)?;
         let name = parse_id(tokens)?;
         let ty = Type::Flags(Flags {
+            span: name.span,
             flags: parse_list(
                 tokens,
                 Token::LeftBrace,
@@ -776,28 +923,52 @@ impl<'a> TypeDef<'a> {
                 },
             )?,
         });
-        Ok(TypeDef { docs, name, ty })
+        Ok(TypeDef {
+            docs,
+            attributes,
+            name,
+            ty,
+        })
     }
 
-    fn parse_resource(tokens: &mut Tokenizer<'a>, docs: Docs<'a>) -> Result<Self> {
+    fn parse_resource(
+        tokens: &mut Tokenizer<'a>,
+        docs: Docs<'a>,
+        attributes: Vec<Attribute<'a>>,
+    ) -> Result<Self> {
         tokens.expect(Token::Resource)?;
         let name = parse_id(tokens)?;
         let mut funcs = Vec::new();
         if tokens.eat(Token::LeftBrace)? {
             while !tokens.eat(Token::RightBrace)? {
-                funcs.push(ResourceFunc::parse(parse_docs(tokens)?, tokens)?);
+                let docs = parse_docs(tokens)?;
+                let attributes = Attribute::parse_list(tokens)?;
+                funcs.push(ResourceFunc::parse(docs, attributes, tokens)?);
             }
         } else {
             tokens.expect_semicolon()?;
         }
-        let ty = Type::Resource(Resource { funcs });
-        Ok(TypeDef { docs, name, ty })
+        let ty = Type::Resource(Resource {
+            span: name.span,
+            funcs,
+        });
+        Ok(TypeDef {
+            docs,
+            attributes,
+            name,
+            ty,
+        })
     }
 
-    fn parse_record(tokens: &mut Tokenizer<'a>, docs: Docs<'a>) -> Result<Self> {
+    fn parse_record(
+        tokens: &mut Tokenizer<'a>,
+        docs: Docs<'a>,
+        attributes: Vec<Attribute<'a>>,
+    ) -> Result<Self> {
         tokens.expect(Token::Record)?;
         let name = parse_id(tokens)?;
         let ty = Type::Record(Record {
+            span: name.span,
             fields: parse_list(
                 tokens,
                 Token::LeftBrace,
@@ -810,10 +981,19 @@ impl<'a> TypeDef<'a> {
                 },
             )?,
         });
-        Ok(TypeDef { docs, name, ty })
+        Ok(TypeDef {
+            docs,
+            attributes,
+            name,
+            ty,
+        })
     }
 
-    fn parse_variant(tokens: &mut Tokenizer<'a>, docs: Docs<'a>) -> Result<Self> {
+    fn parse_variant(
+        tokens: &mut Tokenizer<'a>,
+        docs: Docs<'a>,
+        attributes: Vec<Attribute<'a>>,
+    ) -> Result<Self> {
         tokens.expect(Token::Variant)?;
         let name = parse_id(tokens)?;
         let ty = Type::Variant(Variant {
@@ -835,10 +1015,19 @@ impl<'a> TypeDef<'a> {
                 },
             )?,
         });
-        Ok(TypeDef { docs, name, ty })
+        Ok(TypeDef {
+            docs,
+            attributes,
+            name,
+            ty,
+        })
     }
 
-    fn parse_enum(tokens: &mut Tokenizer<'a>, docs: Docs<'a>) -> Result<Self> {
+    fn parse_enum(
+        tokens: &mut Tokenizer<'a>,
+        docs: Docs<'a>,
+        attributes: Vec<Attribute<'a>>,
+    ) -> Result<Self> {
         tokens.expect(Token::Enum)?;
         let name = parse_id(tokens)?;
         let ty = Type::Enum(Enum {
@@ -853,17 +1042,31 @@ impl<'a> TypeDef<'a> {
                 },
             )?,
         });
-        Ok(TypeDef { docs, name, ty })
+        Ok(TypeDef {
+            docs,
+            attributes,
+            name,
+            ty,
+        })
     }
 }
 
 impl<'a> NamedFunc<'a> {
-    fn parse(tokens: &mut Tokenizer<'a>, docs: Docs<'a>) -> Result<Self> {
+    fn parse(
+        tokens: &mut Tokenizer<'a>,
+        docs: Docs<'a>,
+        attributes: Vec<Attribute<'a>>,
+    ) -> Result<Self> {
         let name = parse_id(tokens)?;
         tokens.expect(Token::Colon)?;
         let func = Func::parse(tokens)?;
         tokens.expect_semicolon()?;
-        Ok(NamedFunc { docs, name, func })
+        Ok(NamedFunc {
+            docs,
+            attributes,
+            name,
+            func,
+        })
     }
 }
 
@@ -882,9 +1085,14 @@ fn parse_id<'a>(tokens: &mut Tokenizer<'a>) -> Result<Id<'a>> {
 }
 
 fn parse_opt_version(tokens: &mut Tokenizer<'_>) -> Result<Option<(Span, Version)>> {
-    if !tokens.eat(Token::At)? {
-        return Ok(None);
+    if tokens.eat(Token::At)? {
+        parse_version(tokens).map(Some)
+    } else {
+        Ok(None)
     }
+}
+
+fn parse_version(tokens: &mut Tokenizer<'_>) -> Result<(Span, Version)> {
     let start = tokens.expect(Token::Integer)?.start;
     tokens.expect(Token::Period)?;
     tokens.expect(Token::Integer)?;
@@ -894,11 +1102,8 @@ fn parse_opt_version(tokens: &mut Tokenizer<'_>) -> Result<Option<(Span, Version
     eat_ids(tokens, Token::Minus, &mut span)?;
     eat_ids(tokens, Token::Plus, &mut span)?;
     let string = tokens.get_span(span);
-    let version = Version::parse(string).map_err(|e| Error {
-        span,
-        msg: e.to_string(),
-    })?;
-    return Ok(Some((span, version)));
+    let version = Version::parse(string).map_err(|e| Error::new(span, e.to_string()))?;
+    return Ok((span, version));
 
     fn eat_ids(tokens: &mut Tokenizer<'_>, prefix: Token, end: &mut Span) -> Result<()> {
         if !tokens.eat(prefix)? {
@@ -962,53 +1167,59 @@ fn parse_docs<'a>(tokens: &mut Tokenizer<'a>) -> Result<Docs<'a>> {
 impl<'a> Type<'a> {
     fn parse(tokens: &mut Tokenizer<'a>) -> Result<Self> {
         match tokens.next()? {
-            Some((_span, Token::U8)) => Ok(Type::U8),
-            Some((_span, Token::U16)) => Ok(Type::U16),
-            Some((_span, Token::U32)) => Ok(Type::U32),
-            Some((_span, Token::U64)) => Ok(Type::U64),
-            Some((_span, Token::S8)) => Ok(Type::S8),
-            Some((_span, Token::S16)) => Ok(Type::S16),
-            Some((_span, Token::S32)) => Ok(Type::S32),
-            Some((_span, Token::S64)) => Ok(Type::S64),
-            Some((_span, Token::F32)) => Ok(Type::F32),
-            Some((_span, Token::F64)) => Ok(Type::F64),
-            Some((_span, Token::Char)) => Ok(Type::Char),
+            Some((span, Token::U8)) => Ok(Type::U8(span)),
+            Some((span, Token::U16)) => Ok(Type::U16(span)),
+            Some((span, Token::U32)) => Ok(Type::U32(span)),
+            Some((span, Token::U64)) => Ok(Type::U64(span)),
+            Some((span, Token::S8)) => Ok(Type::S8(span)),
+            Some((span, Token::S16)) => Ok(Type::S16(span)),
+            Some((span, Token::S32)) => Ok(Type::S32(span)),
+            Some((span, Token::S64)) => Ok(Type::S64(span)),
+            Some((span, Token::F32)) => Ok(Type::F32(span)),
+            Some((span, Token::F64)) => Ok(Type::F64(span)),
+            Some((span, Token::Char)) => Ok(Type::Char(span)),
 
             // tuple<T, U, ...>
-            Some((_span, Token::Tuple)) => {
+            Some((span, Token::Tuple)) => {
                 let types = parse_list(
                     tokens,
                     Token::LessThan,
                     Token::GreaterThan,
                     |_docs, tokens| Type::parse(tokens),
                 )?;
-                Ok(Type::Tuple(types))
+                Ok(Type::Tuple(Tuple { span, types }))
             }
 
-            Some((_span, Token::Bool)) => Ok(Type::Bool),
-            Some((_span, Token::String_)) => Ok(Type::String),
+            Some((span, Token::Bool)) => Ok(Type::Bool(span)),
+            Some((span, Token::String_)) => Ok(Type::String(span)),
 
             // list<T>
-            Some((_span, Token::List)) => {
+            Some((span, Token::List)) => {
                 tokens.expect(Token::LessThan)?;
                 let ty = Type::parse(tokens)?;
                 tokens.expect(Token::GreaterThan)?;
-                Ok(Type::List(Box::new(ty)))
+                Ok(Type::List(List {
+                    span,
+                    ty: Box::new(ty),
+                }))
             }
 
             // option<T>
-            Some((_span, Token::Option_)) => {
+            Some((span, Token::Option_)) => {
                 tokens.expect(Token::LessThan)?;
                 let ty = Type::parse(tokens)?;
                 tokens.expect(Token::GreaterThan)?;
-                Ok(Type::Option(Box::new(ty)))
+                Ok(Type::Option(Option_ {
+                    span,
+                    ty: Box::new(ty),
+                }))
             }
 
             // result<T, E>
             // result<_, E>
             // result<T>
             // result
-            Some((_span, Token::Result_)) => {
+            Some((span, Token::Result_)) => {
                 let mut ok = None;
                 let mut err = None;
 
@@ -1024,26 +1235,26 @@ impl<'a> Type<'a> {
                     };
                     tokens.expect(Token::GreaterThan)?;
                 };
-                Ok(Type::Result(Result_ { ok, err }))
+                Ok(Type::Result(Result_ { span, ok, err }))
             }
 
             // future<T>
             // future
-            Some((_span, Token::Future)) => {
+            Some((span, Token::Future)) => {
                 let mut ty = None;
 
                 if tokens.eat(Token::LessThan)? {
                     ty = Some(Box::new(Type::parse(tokens)?));
                     tokens.expect(Token::GreaterThan)?;
                 };
-                Ok(Type::Future(ty))
+                Ok(Type::Future(Future { span, ty }))
             }
 
             // stream<T, Z>
             // stream<_, Z>
             // stream<T>
             // stream
-            Some((_span, Token::Stream)) => {
+            Some((span, Token::Stream)) => {
                 let mut element = None;
                 let mut end = None;
 
@@ -1059,7 +1270,7 @@ impl<'a> Type<'a> {
                     };
                     tokens.expect(Token::GreaterThan)?;
                 };
-                Ok(Type::Stream(Stream { element, end }))
+                Ok(Type::Stream(Stream { span, element, end }))
             }
 
             // own<T>
@@ -1092,6 +1303,37 @@ impl<'a> Type<'a> {
             other => Err(err_expected(tokens, "a type", other).into()),
         }
     }
+
+    fn span(&self) -> Span {
+        match self {
+            Type::Bool(span)
+            | Type::U8(span)
+            | Type::U16(span)
+            | Type::U32(span)
+            | Type::U64(span)
+            | Type::S8(span)
+            | Type::S16(span)
+            | Type::S32(span)
+            | Type::S64(span)
+            | Type::F32(span)
+            | Type::F64(span)
+            | Type::Char(span)
+            | Type::String(span) => *span,
+            Type::Name(id) => id.span,
+            Type::List(l) => l.span,
+            Type::Handle(h) => h.span(),
+            Type::Resource(r) => r.span,
+            Type::Record(r) => r.span,
+            Type::Flags(f) => f.span,
+            Type::Variant(v) => v.span,
+            Type::Tuple(t) => t.span,
+            Type::Enum(e) => e.span,
+            Type::Option(o) => o.span,
+            Type::Result(r) => r.span,
+            Type::Future(f) => f.span,
+            Type::Stream(s) => s.span,
+        }
+    }
 }
 
 fn parse_list<'a, T>(
@@ -1138,20 +1380,95 @@ fn err_expected(
     found: Option<(Span, Token)>,
 ) -> Error {
     match found {
-        Some((span, token)) => Error {
+        Some((span, token)) => Error::new(
             span,
-            msg: format!("expected {}, found {}", expected, token.describe()),
-        },
-        None => Error {
-            span: Span {
+            format!("expected {}, found {}", expected, token.describe()),
+        ),
+        None => Error::new(
+            Span {
                 start: u32::try_from(tokens.input().len()).unwrap(),
                 end: u32::try_from(tokens.input().len()).unwrap(),
             },
-            msg: format!("expected {}, found eof", expected),
-        },
+            format!("expected {}, found eof", expected),
+        ),
+    }
+}
+
+enum Attribute<'a> {
+    Since {
+        span: Span,
+        version: Version,
+        feature: Option<Id<'a>>,
+    },
+    Unstable {
+        span: Span,
+        feature: Id<'a>,
+    },
+}
+
+impl<'a> Attribute<'a> {
+    fn parse_list(tokens: &mut Tokenizer<'a>) -> Result<Vec<Attribute<'a>>> {
+        let mut ret = Vec::new();
+        while tokens.eat(Token::At)? {
+            let id = parse_id(tokens)?;
+            let attr = match id.name {
+                "since" => {
+                    tokens.eat(Token::LeftParen)?;
+                    eat_id(tokens, "version")?;
+                    tokens.eat(Token::Equals)?;
+                    let (_span, version) = parse_version(tokens)?;
+                    let feature = if tokens.eat(Token::Comma)? {
+                        eat_id(tokens, "feature")?;
+                        tokens.eat(Token::Equals)?;
+                        Some(parse_id(tokens)?)
+                    } else {
+                        None
+                    };
+                    tokens.eat(Token::RightParen)?;
+                    Attribute::Since {
+                        span: id.span,
+                        version,
+                        feature,
+                    }
+                }
+                "unstable" => {
+                    tokens.eat(Token::LeftParen)?;
+                    eat_id(tokens, "feature")?;
+                    tokens.eat(Token::Equals)?;
+                    let feature = parse_id(tokens)?;
+                    tokens.eat(Token::RightParen)?;
+                    Attribute::Unstable {
+                        span: id.span,
+                        feature,
+                    }
+                }
+                other => {
+                    bail!(Error::new(id.span, format!("unknown attribute `{other}`"),))
+                }
+            };
+            ret.push(attr);
+        }
+        Ok(ret)
+    }
+
+    fn span(&self) -> Span {
+        match self {
+            Attribute::Since { span, .. } | Attribute::Unstable { span, .. } => *span,
+        }
     }
 }
 
+fn eat_id(tokens: &mut Tokenizer<'_>, expected: &str) -> Result<Span> {
+    let id = parse_id(tokens)?;
+    if id.name != expected {
+        bail!(Error::new(
+            id.span,
+            format!("expected `{expected}`, found `{}`", id.name),
+        ));
+    }
+    Ok(id.span)
+}
+
 /// A listing of source files which are used to get parsed into an
 /// [`UnresolvedPackage`].
 #[derive(Clone, Default)]
@@ -1241,13 +1558,16 @@ impl SourceMap {
     where
         F: FnOnce() -> Result<T>,
     {
-        let err = match f() {
+        let mut err = match f() {
             Ok(t) => return Ok(t),
             Err(e) => e,
         };
-        if let Some(parse) = err.downcast_ref::<Error>() {
-            let msg = self.highlight_err(parse.span.start, Some(parse.span.end), parse);
-            bail!("{msg}")
+        if let Some(parse) = err.downcast_mut::<Error>() {
+            if parse.highlighted.is_none() {
+                let msg = self.highlight_err(parse.span.start, Some(parse.span.end), &parse.msg);
+                parse.highlighted = Some(msg);
+            }
+            return Err(err);
         }
 
         if let Some(lex) = err.downcast_ref::<lex::Error>() {
diff --git a/crates/wit-parser/src/ast/resolve.rs b/crates/wit-parser/src/ast/resolve.rs
index 0962d05d..4004a846 100644
--- a/crates/wit-parser/src/ast/resolve.rs
+++ b/crates/wit-parser/src/ast/resolve.rs
@@ -64,9 +64,12 @@ pub struct Resolver<'a> {
     /// pointing to it if the item isn't actually defined.
     unknown_type_spans: Vec<Span>,
 
-    /// Spans for each world in `self.world`
+    /// Spans for each world in `self.worlds`
     world_spans: Vec<WorldSpan>,
 
+    /// Spans for each type in `self.types`
+    type_spans: Vec<Span>,
+
     /// The span of each interface's definition which is used for error
     /// reporting during the final `Resolve` phase.
     interface_spans: Vec<InterfaceSpan>,
@@ -114,13 +117,13 @@ impl<'a> Resolver<'a> {
             let cur_name = cur.package_name();
             if let Some(prev) = &self.package_name {
                 if cur_name != *prev {
-                    bail!(Error {
-                        span: cur.span,
-                        msg: format!(
+                    bail!(Error::new(
+                        cur.span,
+                        format!(
                             "package identifier `{cur_name}` does not match \
                              previous package name of `{prev}`"
                         ),
-                    })
+                    ))
                 }
             }
             self.package_name = Some(cur_name);
@@ -129,10 +132,10 @@ impl<'a> Resolver<'a> {
             let docs = self.docs(&cur.docs);
             if docs.contents.is_some() {
                 if self.package_docs.contents.is_some() {
-                    bail!(Error {
-                        span: cur.docs.span,
-                        msg: "found doc comments on multiple 'package' items".into(),
-                    })
+                    bail!(Error::new(
+                        cur.docs.span,
+                        "found doc comments on multiple 'package' items"
+                    ))
                 }
                 self.package_docs = docs;
             }
@@ -187,7 +190,7 @@ impl<'a> Resolver<'a> {
         for id in iface_order {
             let (interface, i) = &iface_id_to_ast[&id];
             self.cur_ast_index = *i;
-            self.resolve_interface(id, &interface.items, &interface.docs)?;
+            self.resolve_interface(id, &interface.items, &interface.docs, &interface.attributes)?;
         }
 
         for id in world_order {
@@ -217,6 +220,7 @@ impl<'a> Resolver<'a> {
             unknown_type_spans: mem::take(&mut self.unknown_type_spans),
             interface_spans: mem::take(&mut self.interface_spans),
             world_spans: mem::take(&mut self.world_spans),
+            type_spans: mem::take(&mut self.type_spans),
             foreign_dep_spans: mem::take(&mut self.foreign_dep_spans),
             source_map: SourceMap::default(),
             required_resource_types: mem::take(&mut self.required_resource_types),
@@ -289,6 +293,7 @@ impl<'a> Resolver<'a> {
             name: None,
             types: IndexMap::new(),
             docs: Docs::default(),
+            stability: Default::default(),
             functions: IndexMap::new(),
             package: None,
         })
@@ -309,6 +314,7 @@ impl<'a> Resolver<'a> {
             package: None,
             includes: Default::default(),
             include_names: Default::default(),
+            stability: Default::default(),
         })
     }
 
@@ -332,10 +338,10 @@ impl<'a> Resolver<'a> {
                 match item {
                     ast::AstItem::Interface(i) => {
                         if package_items.insert(i.name.name, i.name.span).is_some() {
-                            bail!(Error {
-                                span: i.name.span,
-                                msg: format!("duplicate item named `{}`", i.name.name),
-                            })
+                            bail!(Error::new(
+                                i.name.span,
+                                format!("duplicate item named `{}`", i.name.name),
+                            ))
                         }
                         let prev = ast_ns.insert(i.name.name, ());
                         assert!(prev.is_none());
@@ -346,10 +352,10 @@ impl<'a> Resolver<'a> {
                     }
                     ast::AstItem::World(w) => {
                         if package_items.insert(w.name.name, w.name.span).is_some() {
-                            bail!(Error {
-                                span: w.name.span,
-                                msg: format!("duplicate item named `{}`", w.name.name),
-                            })
+                            bail!(Error::new(
+                                w.name.span,
+                                format!("duplicate item named `{}`", w.name.name),
+                            ))
                         }
                         let prev = ast_ns.insert(w.name.name, ());
                         assert!(prev.is_none());
@@ -394,10 +400,10 @@ impl<'a> Resolver<'a> {
                     ast::AstItem::World(w) => (&w.name, ItemSource::Local(w.name.clone())),
                 };
                 if ast_ns.insert(name.name, (name.span, src)).is_some() {
-                    bail!(Error {
-                        span: name.span,
-                        msg: format!("duplicate name `{}` in this file", name.name),
-                    });
+                    bail!(Error::new(
+                        name.span,
+                        format!("duplicate name `{}` in this file", name.name),
+                    ));
                 }
             }
 
@@ -425,13 +431,13 @@ impl<'a> Resolver<'a> {
                             order[iface.name].push(used_name.clone());
                         }
                         None => {
-                            bail!(Error {
-                                span: used_name.span,
-                                msg: format!(
+                            bail!(Error::new(
+                                used_name.span,
+                                format!(
                                     "interface or world `{name}` not found in package",
                                     name = used_name.name
                                 ),
-                            })
+                            ))
                         }
                     },
                 }
@@ -472,14 +478,22 @@ impl<'a> Resolver<'a> {
             for item in ast.items.iter() {
                 let (name, ast_item) = match item {
                     ast::AstItem::Use(u) => {
+                        if !u.attributes.is_empty() {
+                            bail!(Error::new(
+                                u.span,
+                                format!("attributes not allowed on top-level use"),
+                            ))
+                        }
                         let name = u.as_.as_ref().unwrap_or(u.item.name());
                         let item = match &u.item {
-                            ast::UsePath::Id(name) => *ids.get(name.name).ok_or_else(|| Error {
-                                span: name.span,
-                                msg: format!(
-                                    "interface or world `{name}` does not exist",
-                                    name = name.name
-                                ),
+                            ast::UsePath::Id(name) => *ids.get(name.name).ok_or_else(|| {
+                                Error::new(
+                                    name.span,
+                                    format!(
+                                        "interface or world `{name}` does not exist",
+                                        name = name.name
+                                    ),
+                                )
                             })?,
                             ast::UsePath::Package { id, name } => {
                                 self.foreign_deps[&id.package_name()][name.name]
@@ -543,11 +557,13 @@ impl<'a> Resolver<'a> {
                     }
                     let id = self.types.alloc(TypeDef {
                         docs: Docs::default(),
+                        stability: Default::default(),
                         kind: TypeDefKind::Unknown,
                         name: Some(name.name.name.to_string()),
                         owner: TypeOwner::Interface(iface),
                     });
                     self.unknown_type_spans.push(name.name.span);
+                    self.type_spans.push(name.name.span);
                     lookup.insert(name.name.name, (TypeOrItem::Type(id), name.name.span));
                     self.interfaces[iface]
                         .types
@@ -563,6 +579,8 @@ impl<'a> Resolver<'a> {
     fn resolve_world(&mut self, world_id: WorldId, world: &ast::World<'a>) -> Result<WorldId> {
         let docs = self.docs(&world.docs);
         self.worlds[world_id].docs = docs;
+        let stability = self.stability(&world.attributes)?;
+        self.worlds[world_id].stability = stability;
 
         self.resolve_types(
             TypeOwner::World(world_id),
@@ -593,13 +611,10 @@ impl<'a> Resolver<'a> {
                 TypeOrItem::Type(id) => {
                     let prev = import_names.insert(*name, "import");
                     if let Some(prev) = prev {
-                        return Err(Error {
-                            span: *span,
-                            msg: format!(
-                                "import `{name}` conflicts with prior {prev} of same name",
-                            ),
-                        }
-                        .into());
+                        bail!(Error::new(
+                            *span,
+                            format!("import `{name}` conflicts with prior {prev} of same name",),
+                        ))
                     }
                     self.worlds[world_id]
                         .imports
@@ -613,9 +628,10 @@ impl<'a> Resolver<'a> {
         let mut imported_interfaces = HashSet::new();
         let mut exported_interfaces = HashSet::new();
         for item in world.items.iter() {
-            let (docs, kind, desc, spans, interfaces, names) = match item {
+            let (docs, attrs, kind, desc, spans, interfaces, names) = match item {
                 ast::WorldItem::Import(import) => (
                     &import.docs,
+                    &import.attributes,
                     &import.kind,
                     "import",
                     &mut import_spans,
@@ -624,6 +640,7 @@ impl<'a> Resolver<'a> {
                 ),
                 ast::WorldItem::Export(export) => (
                     &export.docs,
+                    &export.attributes,
                     &export.kind,
                     "export",
                     &mut export_spans,
@@ -659,14 +676,13 @@ impl<'a> Resolver<'a> {
                 ast::ExternKind::Interface(name, _) | ast::ExternKind::Func(name, _) => {
                     let prev = names.insert(name.name, desc);
                     if let Some(prev) = prev {
-                        return Err(Error {
-                            span: kind.span(),
-                            msg: format!(
+                        bail!(Error::new(
+                            kind.span(),
+                            format!(
                                 "{desc} `{name}` conflicts with prior {prev} of same name",
                                 name = name.name
                             ),
-                        }
-                        .into());
+                        ))
                     }
                     WorldKey::Name(name.name.to_string())
                 }
@@ -676,13 +692,13 @@ impl<'a> Resolver<'a> {
                     WorldKey::Interface(id)
                 }
             };
-            let world_item = self.resolve_world_item(docs, kind)?;
-            if let WorldItem::Interface(id) = world_item {
+            let world_item = self.resolve_world_item(docs, attrs, kind)?;
+            if let WorldItem::Interface { id, .. } = world_item {
                 if !interfaces.insert(id) {
-                    bail!(Error {
-                        span: kind.span(),
-                        msg: format!("interface cannot be {desc}ed more than once"),
-                    })
+                    bail!(Error::new(
+                        kind.span(),
+                        format!("interface cannot be {desc}ed more than once"),
+                    ))
                 }
             }
             let dst = if desc == "import" {
@@ -704,24 +720,32 @@ impl<'a> Resolver<'a> {
     fn resolve_world_item(
         &mut self,
         docs: &ast::Docs<'a>,
+        attrs: &[ast::Attribute<'a>],
         kind: &ast::ExternKind<'a>,
     ) -> Result<WorldItem> {
         match kind {
             ast::ExternKind::Interface(name, items) => {
                 let prev = mem::take(&mut self.type_lookup);
                 let id = self.alloc_interface(name.span);
-                self.resolve_interface(id, items, docs)?;
+                self.resolve_interface(id, items, docs, attrs)?;
                 self.type_lookup = prev;
-                Ok(WorldItem::Interface(id))
+                let stability = self.interfaces[id].stability.clone();
+                Ok(WorldItem::Interface { id, stability })
             }
             ast::ExternKind::Path(path) => {
+                let stability = self.stability(attrs)?;
                 let (item, name, span) = self.resolve_ast_item_path(path)?;
                 let id = self.extract_iface_from_item(&item, &name, span)?;
-                Ok(WorldItem::Interface(id))
+                Ok(WorldItem::Interface { id, stability })
             }
             ast::ExternKind::Func(name, func) => {
-                let func =
-                    self.resolve_function(docs, name.name, func, FunctionKind::Freestanding)?;
+                let func = self.resolve_function(
+                    docs,
+                    attrs,
+                    name.name,
+                    func,
+                    FunctionKind::Freestanding,
+                )?;
                 Ok(WorldItem::Function(func))
             }
         }
@@ -732,9 +756,12 @@ impl<'a> Resolver<'a> {
         interface_id: InterfaceId,
         fields: &[ast::InterfaceItem<'a>],
         docs: &ast::Docs<'a>,
+        attrs: &[ast::Attribute<'a>],
     ) -> Result<()> {
         let docs = self.docs(docs);
         self.interfaces[interface_id].docs = docs;
+        let stability = self.stability(attrs)?;
+        self.interfaces[interface_id].stability = stability;
 
         self.resolve_types(
             TypeOwner::Interface(interface_id),
@@ -765,6 +792,7 @@ impl<'a> Resolver<'a> {
                     self.define_interface_name(&f.name, TypeOrItem::Item("function"))?;
                     funcs.push(self.resolve_function(
                         &f.docs,
+                        &f.attributes,
                         &f.name.name,
                         &f.func,
                         FunctionKind::Freestanding,
@@ -833,11 +861,10 @@ impl<'a> Resolver<'a> {
                 TypeItem::Def(t) => {
                     let prev = type_defs.insert(t.name.name, Some(t));
                     if prev.is_some() {
-                        return Err(Error {
-                            span: t.name.span,
-                            msg: format!("name `{}` is defined more than once", t.name.name),
-                        }
-                        .into());
+                        bail!(Error::new(
+                            t.name.span,
+                            format!("name `{}` is defined more than once", t.name.name),
+                        ))
                     }
                     let mut deps = Vec::new();
                     collect_deps(&t.ty, &mut deps);
@@ -859,13 +886,16 @@ impl<'a> Resolver<'a> {
                 None => continue,
             };
             let docs = self.docs(&def.docs);
-            let kind = self.resolve_type_def(&def.ty)?;
+            let stability = self.stability(&def.attributes)?;
+            let kind = self.resolve_type_def(&def.ty, &stability)?;
             let id = self.types.alloc(TypeDef {
                 docs,
+                stability,
                 kind,
                 name: Some(def.name.name.to_string()),
                 owner,
             });
+            self.type_spans.push(def.name.span);
             self.define_interface_name(&def.name, TypeOrItem::Type(id))?;
         }
         Ok(())
@@ -874,25 +904,28 @@ impl<'a> Resolver<'a> {
     fn resolve_use(&mut self, owner: TypeOwner, u: &ast::Use<'a>) -> Result<()> {
         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)?;
 
         for name in u.names.iter() {
             let lookup = &self.interface_types[use_from.index()];
             let id = match lookup.get(name.name.name) {
                 Some((TypeOrItem::Type(id), _)) => *id,
                 Some((TypeOrItem::Item(s), _)) => {
-                    bail!(Error {
-                        span: name.name.span,
-                        msg: format!("cannot import {s} `{}`", name.name.name),
-                    })
+                    bail!(Error::new(
+                        name.name.span,
+                        format!("cannot import {s} `{}`", name.name.name),
+                    ))
                 }
-                None => bail!(Error {
-                    span: name.name.span,
-                    msg: format!("name `{}` is not defined", name.name.name),
-                }),
+                None => bail!(Error::new(
+                    name.name.span,
+                    format!("name `{}` is not defined", name.name.name),
+                )),
             };
+            self.type_spans.push(name.name.span);
             let name = name.as_.as_ref().unwrap_or(&name.name);
             let id = self.types.alloc(TypeDef {
                 docs: Docs::default(),
+                stability: stability.clone(),
                 kind: TypeDefKind::Type(Type::Id(id)),
                 name: Some(name.name.to_string()),
                 owner,
@@ -904,9 +937,12 @@ impl<'a> Resolver<'a> {
 
     /// For each name in the `include`, resolve the path of the include, add it to the self.includes
     fn resolve_include(&mut self, world_id: WorldId, i: &ast::Include<'a>) -> Result<()> {
+        let stability = self.stability(&i.attributes)?;
         let (item, name, span) = self.resolve_ast_item_path(&i.from)?;
         let include_from = self.extract_world_from_item(&item, &name, span)?;
-        self.worlds[world_id].includes.push(include_from);
+        self.worlds[world_id]
+            .includes
+            .push((stability, include_from));
         self.worlds[world_id].include_names.push(
             i.names
                 .iter()
@@ -945,21 +981,30 @@ impl<'a> Resolver<'a> {
             }
         }
         let named_func = func.named_func();
-        self.resolve_function(&named_func.docs, &name, &named_func.func, kind)
+        self.resolve_function(
+            &named_func.docs,
+            &named_func.attributes,
+            &name,
+            &named_func.func,
+            kind,
+        )
     }
 
     fn resolve_function(
         &mut self,
         docs: &ast::Docs<'_>,
+        attrs: &[ast::Attribute<'_>],
         name: &str,
         func: &ast::Func,
         kind: FunctionKind,
     ) -> Result<Function> {
         let docs = self.docs(docs);
-        let params = self.resolve_params(&func.params, &kind)?;
-        let results = self.resolve_results(&func.results, &kind)?;
+        let stability = self.stability(attrs)?;
+        let params = self.resolve_params(&func.params, &kind, func.span, &stability)?;
+        let results = self.resolve_results(&func.results, &kind, func.span, &stability)?;
         Ok(Function {
             docs,
+            stability,
             name: name.to_string(),
             kind,
             params,
@@ -976,10 +1021,10 @@ impl<'a> Resolver<'a> {
                 match item {
                     Some(item) => Ok((*item, id.name.into(), id.span)),
                     None => {
-                        bail!(Error {
-                            span: id.span,
-                            msg: format!("interface or world `{}` does not exist", id.name),
-                        })
+                        bail!(Error::new(
+                            id.span,
+                            format!("interface or world `{}` does not exist", id.name),
+                        ))
                     }
                 }
             }
@@ -1000,10 +1045,10 @@ impl<'a> Resolver<'a> {
         match item {
             AstItem::Interface(id) => Ok(*id),
             AstItem::World(_) => {
-                bail!(Error {
-                    span: span,
-                    msg: format!("name `{}` is defined as a world, not an interface", name),
-                })
+                bail!(Error::new(
+                    span,
+                    format!("name `{}` is defined as a world, not an interface", name),
+                ))
             }
         }
     }
@@ -1012,10 +1057,10 @@ impl<'a> Resolver<'a> {
         match item {
             AstItem::World(id) => Ok(*id),
             AstItem::Interface(_) => {
-                bail!(Error {
-                    span: span,
-                    msg: format!("name `{}` is defined as an interface, not a world", name),
-                })
+                bail!(Error::new(
+                    span,
+                    format!("name `{}` is defined as an interface, not a world", name),
+                ))
             }
         }
     }
@@ -1023,37 +1068,40 @@ impl<'a> Resolver<'a> {
     fn define_interface_name(&mut self, name: &ast::Id<'a>, item: TypeOrItem) -> Result<()> {
         let prev = self.type_lookup.insert(name.name, (item, name.span));
         if prev.is_some() {
-            Err(Error {
-                span: name.span,
-                msg: format!("name `{}` is defined more than once", name.name),
-            }
-            .into())
+            bail!(Error::new(
+                name.span,
+                format!("name `{}` is defined more than once", name.name),
+            ))
         } else {
             Ok(())
         }
     }
 
-    fn resolve_type_def(&mut self, ty: &ast::Type<'_>) -> Result<TypeDefKind> {
+    fn resolve_type_def(
+        &mut self,
+        ty: &ast::Type<'_>,
+        stability: &Stability,
+    ) -> Result<TypeDefKind> {
         Ok(match ty {
-            ast::Type::Bool => TypeDefKind::Type(Type::Bool),
-            ast::Type::U8 => TypeDefKind::Type(Type::U8),
-            ast::Type::U16 => TypeDefKind::Type(Type::U16),
-            ast::Type::U32 => TypeDefKind::Type(Type::U32),
-            ast::Type::U64 => TypeDefKind::Type(Type::U64),
-            ast::Type::S8 => TypeDefKind::Type(Type::S8),
-            ast::Type::S16 => TypeDefKind::Type(Type::S16),
-            ast::Type::S32 => TypeDefKind::Type(Type::S32),
-            ast::Type::S64 => TypeDefKind::Type(Type::S64),
-            ast::Type::F32 => TypeDefKind::Type(Type::F32),
-            ast::Type::F64 => TypeDefKind::Type(Type::F64),
-            ast::Type::Char => TypeDefKind::Type(Type::Char),
-            ast::Type::String => TypeDefKind::Type(Type::String),
+            ast::Type::Bool(_) => TypeDefKind::Type(Type::Bool),
+            ast::Type::U8(_) => TypeDefKind::Type(Type::U8),
+            ast::Type::U16(_) => TypeDefKind::Type(Type::U16),
+            ast::Type::U32(_) => TypeDefKind::Type(Type::U32),
+            ast::Type::U64(_) => TypeDefKind::Type(Type::U64),
+            ast::Type::S8(_) => TypeDefKind::Type(Type::S8),
+            ast::Type::S16(_) => TypeDefKind::Type(Type::S16),
+            ast::Type::S32(_) => TypeDefKind::Type(Type::S32),
+            ast::Type::S64(_) => TypeDefKind::Type(Type::S64),
+            ast::Type::F32(_) => TypeDefKind::Type(Type::F32),
+            ast::Type::F64(_) => TypeDefKind::Type(Type::F64),
+            ast::Type::Char(_) => TypeDefKind::Type(Type::Char),
+            ast::Type::String(_) => TypeDefKind::Type(Type::String),
             ast::Type::Name(name) => {
                 let id = self.resolve_type_name(name)?;
                 TypeDefKind::Type(Type::Id(id))
             }
             ast::Type::List(list) => {
-                let ty = self.resolve_type(list)?;
+                let ty = self.resolve_type(&list.ty, stability)?;
                 TypeDefKind::List(ty)
             }
             ast::Type::Handle(handle) => TypeDefKind::Handle(match handle {
@@ -1071,19 +1119,16 @@ impl<'a> Resolver<'a> {
                     match func {
                         ast::ResourceFunc::Method(f) | ast::ResourceFunc::Static(f) => {
                             if !names.insert(&f.name.name) {
-                                bail!(Error {
-                                    span: f.name.span,
-                                    msg: format!("duplicate function name `{}`", f.name.name),
-                                })
+                                bail!(Error::new(
+                                    f.name.span,
+                                    format!("duplicate function name `{}`", f.name.name),
+                                ))
                             }
                         }
                         ast::ResourceFunc::Constructor(f) => {
                             ctors += 1;
                             if ctors > 1 {
-                                bail!(Error {
-                                    span: f.name.span,
-                                    msg: format!("duplicate constructors"),
-                                })
+                                bail!(Error::new(f.name.span, "duplicate constructors"))
                             }
                         }
                     }
@@ -1099,7 +1144,7 @@ impl<'a> Resolver<'a> {
                         Ok(Field {
                             docs: self.docs(&field.docs),
                             name: field.name.name.to_string(),
-                            ty: self.resolve_type(&field.ty)?,
+                            ty: self.resolve_type(&field.ty, stability)?,
                         })
                     })
                     .collect::<Result<Vec<_>>>()?;
@@ -1116,20 +1161,17 @@ impl<'a> Resolver<'a> {
                     .collect::<Vec<_>>();
                 TypeDefKind::Flags(Flags { flags })
             }
-            ast::Type::Tuple(types) => {
-                let types = types
+            ast::Type::Tuple(t) => {
+                let types = t
+                    .types
                     .iter()
-                    .map(|ty| self.resolve_type(ty))
+                    .map(|ty| self.resolve_type(ty, stability))
                     .collect::<Result<Vec<_>>>()?;
                 TypeDefKind::Tuple(Tuple { types })
             }
             ast::Type::Variant(variant) => {
                 if variant.cases.is_empty() {
-                    return Err(Error {
-                        span: variant.span,
-                        msg: "empty variant".to_string(),
-                    }
-                    .into());
+                    bail!(Error::new(variant.span, "empty variant"))
                 }
                 let cases = variant
                     .cases
@@ -1138,7 +1180,7 @@ impl<'a> Resolver<'a> {
                         Ok(Case {
                             docs: self.docs(&case.docs),
                             name: case.name.name.to_string(),
-                            ty: self.resolve_optional_type(case.ty.as_ref())?,
+                            ty: self.resolve_optional_type(case.ty.as_ref(), stability)?,
                         })
                     })
                     .collect::<Result<Vec<_>>>()?;
@@ -1146,11 +1188,7 @@ impl<'a> Resolver<'a> {
             }
             ast::Type::Enum(e) => {
                 if e.cases.is_empty() {
-                    return Err(Error {
-                        span: e.span,
-                        msg: "empty enum".to_string(),
-                    }
-                    .into());
+                    bail!(Error::new(e.span, "empty enum"))
                 }
                 let cases = e
                     .cases
@@ -1164,15 +1202,17 @@ impl<'a> Resolver<'a> {
                     .collect::<Result<Vec<_>>>()?;
                 TypeDefKind::Enum(Enum { cases })
             }
-            ast::Type::Option(ty) => TypeDefKind::Option(self.resolve_type(ty)?),
+            ast::Type::Option(ty) => TypeDefKind::Option(self.resolve_type(&ty.ty, stability)?),
             ast::Type::Result(r) => TypeDefKind::Result(Result_ {
-                ok: self.resolve_optional_type(r.ok.as_deref())?,
-                err: self.resolve_optional_type(r.err.as_deref())?,
+                ok: self.resolve_optional_type(r.ok.as_deref(), stability)?,
+                err: self.resolve_optional_type(r.err.as_deref(), stability)?,
             }),
-            ast::Type::Future(t) => TypeDefKind::Future(self.resolve_optional_type(t.as_deref())?),
+            ast::Type::Future(t) => {
+                TypeDefKind::Future(self.resolve_optional_type(t.ty.as_deref(), stability)?)
+            }
             ast::Type::Stream(s) => TypeDefKind::Stream(Stream {
-                element: self.resolve_optional_type(s.element.as_deref())?,
-                end: self.resolve_optional_type(s.end.as_deref())?,
+                element: self.resolve_optional_type(s.element.as_deref(), stability)?,
+                end: self.resolve_optional_type(s.end.as_deref(), stability)?,
             }),
         })
     }
@@ -1180,14 +1220,14 @@ impl<'a> Resolver<'a> {
     fn resolve_type_name(&mut self, name: &ast::Id<'_>) -> Result<TypeId> {
         match self.type_lookup.get(name.name) {
             Some((TypeOrItem::Type(id), _)) => Ok(*id),
-            Some((TypeOrItem::Item(s), _)) => bail!(Error {
-                span: name.span,
-                msg: format!("cannot use {s} `{name}` as a type", name = name.name),
-            }),
-            None => bail!(Error {
-                span: name.span,
-                msg: format!("name `{name}` is not defined", name = name.name),
-            }),
+            Some((TypeOrItem::Item(s), _)) => bail!(Error::new(
+                name.span,
+                format!("cannot use {s} `{name}` as a type", name = name.name),
+            )),
+            None => bail!(Error::new(
+                name.span,
+                format!("name `{name}` is not defined", name = name.name),
+            )),
         }
     }
 
@@ -1202,15 +1242,15 @@ impl<'a> Resolver<'a> {
                     self.required_resource_types.push((cur, name.span));
                     break Ok(id);
                 }
-                _ => bail!(Error {
-                    span: name.span,
-                    msg: format!("type `{}` used in a handle must be a resource", name.name),
-                }),
+                _ => bail!(Error::new(
+                    name.span,
+                    format!("type `{}` used in a handle must be a resource", name.name),
+                )),
             }
         }
     }
 
-    fn resolve_type(&mut self, ty: &super::Type<'_>) -> Result<Type> {
+    fn resolve_type(&mut self, ty: &super::Type<'_>, stability: &Stability) -> Result<Type> {
         // Resources must be declared at the top level to have their methods
         // processed appropriately, but resources also shouldn't show up
         // recursively so assert that's not happening here.
@@ -1218,23 +1258,31 @@ impl<'a> Resolver<'a> {
             ast::Type::Resource(_) => unreachable!(),
             _ => {}
         }
-        let kind = self.resolve_type_def(ty)?;
-        Ok(self.anon_type_def(TypeDef {
-            kind,
-            name: None,
-            docs: Docs::default(),
-            owner: TypeOwner::None,
-        }))
+        let kind = self.resolve_type_def(ty, stability)?;
+        Ok(self.anon_type_def(
+            TypeDef {
+                kind,
+                name: None,
+                docs: Docs::default(),
+                stability: stability.clone(),
+                owner: TypeOwner::None,
+            },
+            ty.span(),
+        ))
     }
 
-    fn resolve_optional_type(&mut self, ty: Option<&super::Type<'_>>) -> Result<Option<Type>> {
+    fn resolve_optional_type(
+        &mut self,
+        ty: Option<&super::Type<'_>>,
+        stability: &Stability,
+    ) -> Result<Option<Type>> {
         match ty {
-            Some(ty) => Ok(Some(self.resolve_type(ty)?)),
+            Some(ty) => Ok(Some(self.resolve_type(ty, stability)?)),
             None => Ok(None),
         }
     }
 
-    fn anon_type_def(&mut self, ty: TypeDef) -> Type {
+    fn anon_type_def(&mut self, ty: TypeDef, span: Span) -> Type {
         let key = match &ty.kind {
             TypeDefKind::Type(t) => return *t,
             TypeDefKind::Variant(v) => Key::Variant(
@@ -1270,11 +1318,10 @@ impl<'a> Resolver<'a> {
             TypeDefKind::Stream(s) => Key::Stream(s.element, s.end),
             TypeDefKind::Unknown => unreachable!(),
         };
-        let types = &mut self.types;
-        let id = self
-            .anon_types
-            .entry(key)
-            .or_insert_with(|| types.alloc(ty));
+        let id = self.anon_types.entry(key).or_insert_with(|| {
+            self.type_spans.push(span);
+            self.types.alloc(ty)
+        });
         Type::Id(*id)
     }
 
@@ -1311,7 +1358,34 @@ impl<'a> Resolver<'a> {
         Docs { contents }
     }
 
-    fn resolve_params(&mut self, params: &ParamList<'_>, kind: &FunctionKind) -> Result<Params> {
+    fn stability(&mut self, attrs: &[ast::Attribute<'_>]) -> Result<Stability> {
+        match attrs {
+            [] => Ok(Stability::Unknown),
+            [ast::Attribute::Since {
+                version, feature, ..
+            }] => Ok(Stability::Stable {
+                since: version.clone(),
+                feature: feature.as_ref().map(|s| s.name.to_string()),
+            }),
+            [ast::Attribute::Unstable { feature, .. }] => Ok(Stability::Unstable {
+                feature: feature.name.to_string(),
+            }),
+            [_, b, ..] => {
+                bail!(Error::new(
+                    b.span(),
+                    "only one stability attribute is allowed per-item",
+                ))
+            }
+        }
+    }
+
+    fn resolve_params(
+        &mut self,
+        params: &ParamList<'_>,
+        kind: &FunctionKind,
+        span: Span,
+        stability: &Stability,
+    ) -> Result<Params> {
         let mut ret = IndexMap::new();
         match *kind {
             // These kinds of methods don't have any adjustments to the
@@ -1322,23 +1396,26 @@ impl<'a> Resolver<'a> {
             // Methods automatically get a `self` initial argument so insert
             // that here before processing the normal parameters.
             FunctionKind::Method(id) => {
-                let shared = self.anon_type_def(TypeDef {
-                    docs: Docs::default(),
-                    kind: TypeDefKind::Handle(Handle::Borrow(id)),
-                    name: None,
-                    owner: TypeOwner::None,
-                });
+                let shared = self.anon_type_def(
+                    TypeDef {
+                        docs: Docs::default(),
+                        stability: stability.clone(),
+                        kind: TypeDefKind::Handle(Handle::Borrow(id)),
+                        name: None,
+                        owner: TypeOwner::None,
+                    },
+                    span,
+                );
                 ret.insert("self".to_string(), shared);
             }
         }
         for (name, ty) in params {
-            let prev = ret.insert(name.name.to_string(), self.resolve_type(ty)?);
+            let prev = ret.insert(name.name.to_string(), self.resolve_type(ty, stability)?);
             if prev.is_some() {
-                return Err(Error {
-                    span: name.span,
-                    msg: format!("param `{}` is defined more than once", name.name),
-                }
-                .into());
+                bail!(Error::new(
+                    name.span,
+                    format!("param `{}` is defined more than once", name.name),
+                ))
             }
         }
         Ok(ret.into_iter().collect())
@@ -1348,16 +1425,21 @@ impl<'a> Resolver<'a> {
         &mut self,
         results: &ResultList<'_>,
         kind: &FunctionKind,
+        span: Span,
+        stability: &Stability,
     ) -> Result<Results> {
         match *kind {
             // These kinds of methods don't have any adjustments to the return
             // values, so plumb them through as-is.
             FunctionKind::Freestanding | FunctionKind::Method(_) | FunctionKind::Static(_) => {
                 match results {
-                    ResultList::Named(rs) => Ok(Results::Named(
-                        self.resolve_params(rs, &FunctionKind::Freestanding)?,
-                    )),
-                    ResultList::Anon(ty) => Ok(Results::Anon(self.resolve_type(ty)?)),
+                    ResultList::Named(rs) => Ok(Results::Named(self.resolve_params(
+                        rs,
+                        &FunctionKind::Freestanding,
+                        span,
+                        stability,
+                    )?)),
+                    ResultList::Anon(ty) => Ok(Results::Anon(self.resolve_type(ty, stability)?)),
                 }
             }
 
@@ -1377,23 +1459,23 @@ impl<'a> Resolver<'a> {
 
 fn collect_deps<'a>(ty: &ast::Type<'a>, deps: &mut Vec<ast::Id<'a>>) {
     match ty {
-        ast::Type::Bool
-        | ast::Type::U8
-        | ast::Type::U16
-        | ast::Type::U32
-        | ast::Type::U64
-        | ast::Type::S8
-        | ast::Type::S16
-        | ast::Type::S32
-        | ast::Type::S64
-        | ast::Type::F32
-        | ast::Type::F64
-        | ast::Type::Char
-        | ast::Type::String
+        ast::Type::Bool(_)
+        | ast::Type::U8(_)
+        | ast::Type::U16(_)
+        | ast::Type::U32(_)
+        | ast::Type::U64(_)
+        | ast::Type::S8(_)
+        | ast::Type::S16(_)
+        | ast::Type::S32(_)
+        | ast::Type::S64(_)
+        | ast::Type::F32(_)
+        | ast::Type::F64(_)
+        | ast::Type::Char(_)
+        | ast::Type::String(_)
         | ast::Type::Flags(_)
         | ast::Type::Enum(_) => {}
         ast::Type::Name(name) => deps.push(name.clone()),
-        ast::Type::List(list) => collect_deps(list, deps),
+        ast::Type::List(list) => collect_deps(&list.ty, deps),
         ast::Type::Handle(handle) => match handle {
             ast::Handle::Own { resource } => deps.push(resource.clone()),
             ast::Handle::Borrow { resource } => deps.push(resource.clone()),
@@ -1404,8 +1486,8 @@ fn collect_deps<'a>(ty: &ast::Type<'a>, deps: &mut Vec<ast::Id<'a>>) {
                 collect_deps(&field.ty, deps);
             }
         }
-        ast::Type::Tuple(types) => {
-            for ty in types {
+        ast::Type::Tuple(t) => {
+            for ty in t.types.iter() {
                 collect_deps(ty, deps);
             }
         }
@@ -1416,7 +1498,7 @@ fn collect_deps<'a>(ty: &ast::Type<'a>, deps: &mut Vec<ast::Id<'a>>) {
                 }
             }
         }
-        ast::Type::Option(ty) => collect_deps(ty, deps),
+        ast::Type::Option(ty) => collect_deps(&ty.ty, deps),
         ast::Type::Result(r) => {
             if let Some(ty) = &r.ok {
                 collect_deps(ty, deps);
@@ -1426,7 +1508,7 @@ fn collect_deps<'a>(ty: &ast::Type<'a>, deps: &mut Vec<ast::Id<'a>>) {
             }
         }
         ast::Type::Future(t) => {
-            if let Some(t) = t {
+            if let Some(t) = &t.ty {
                 collect_deps(t, deps)
             }
         }
diff --git a/crates/wit-parser/src/decoding.rs b/crates/wit-parser/src/decoding.rs
index 8c3c9e01..848aa6ea 100644
--- a/crates/wit-parser/src/decoding.rs
+++ b/crates/wit-parser/src/decoding.rs
@@ -19,8 +19,8 @@ struct ComponentInfo {
     types: types::Types,
     /// List of all imports and exports from this component.
     externs: Vec<(String, Extern)>,
-    /// Decoded package docs
-    package_docs: Option<PackageDocs>,
+    /// Decoded package metadata
+    package_metadata: Option<PackageMetadata>,
 }
 
 struct DecodingExport {
@@ -48,7 +48,7 @@ impl ComponentInfo {
         let mut externs = Vec::new();
         let mut depth = 1;
         let mut types = None;
-        let mut _package_docs = None;
+        let mut _package_metadata = None;
         let mut cur = Parser::new(0);
         let mut eof = false;
         let mut stack = Vec::new();
@@ -111,11 +111,11 @@ impl ComponentInfo {
                     }
                 }
                 #[cfg(feature = "serde")]
-                Payload::CustomSection(s) if s.name() == PackageDocs::SECTION_NAME => {
-                    if _package_docs.is_some() {
-                        bail!("multiple {:?} sections", PackageDocs::SECTION_NAME);
+                Payload::CustomSection(s) if s.name() == PackageMetadata::SECTION_NAME => {
+                    if _package_metadata.is_some() {
+                        bail!("multiple {:?} sections", PackageMetadata::SECTION_NAME);
                     }
-                    _package_docs = Some(PackageDocs::decode(s.data())?);
+                    _package_metadata = Some(PackageMetadata::decode(s.data())?);
                 }
                 Payload::ModuleSection { parser, .. }
                 | Payload::ComponentSection { parser, .. } => {
@@ -140,7 +140,7 @@ impl ComponentInfo {
         Ok(Self {
             types: types.unwrap(),
             externs,
-            package_docs: _package_docs,
+            package_metadata: _package_metadata,
         })
     }
 
@@ -204,8 +204,8 @@ impl ComponentInfo {
 
         let pkg = pkg.ok_or_else(|| anyhow!("no exported component type found"))?;
         let (mut resolve, package) = decoder.finish(pkg);
-        if let Some(package_docs) = &self.package_docs {
-            package_docs.inject(&mut resolve, package)?;
+        if let Some(package_metadata) = &self.package_metadata {
+            package_metadata.inject(&mut resolve, package)?;
         }
         Ok((resolve, package))
     }
@@ -284,8 +284,8 @@ impl ComponentInfo {
         };
 
         let (mut resolve, package) = decoder.finish(pkg);
-        if let Some(package_docs) = &self.package_docs {
-            package_docs.inject(&mut resolve, package)?;
+        if let Some(package_metadata) = &self.package_metadata {
+            package_metadata.inject(&mut resolve, package)?;
         }
         Ok((resolve, package))
     }
@@ -305,6 +305,7 @@ impl ComponentInfo {
             package: None,
             includes: Default::default(),
             include_names: Default::default(),
+            stability: Default::default(),
         });
         let mut package = Package {
             // Similar to `world_name` above this is arbitrarily chosen as it's
@@ -651,7 +652,13 @@ impl WitPackageDecoder<'_> {
                     self.register_interface(name, ty, package)
                         .with_context(|| format!("failed to decode WIT from import `{name}`"))?
                 };
-                (name, WorldItem::Interface(id))
+                (
+                    name,
+                    WorldItem::Interface {
+                        id,
+                        stability: Default::default(),
+                    },
+                )
             }
             types::ComponentEntityType::Func(i) => {
                 let ty = &self.types[i];
@@ -704,7 +711,13 @@ impl WitPackageDecoder<'_> {
                     self.register_interface(name, ty, package)
                         .with_context(|| format!("failed to decode WIT from export `{name}`"))?
                 };
-                (name, WorldItem::Interface(id))
+                (
+                    name,
+                    WorldItem::Interface {
+                        id,
+                        stability: Default::default(),
+                    },
+                )
             }
             _ => {
                 bail!("component export `{name}` was not a function or instance")
@@ -884,6 +897,7 @@ impl WitPackageDecoder<'_> {
                     types: IndexMap::default(),
                     functions: IndexMap::new(),
                     package: None,
+                    stability: Default::default(),
                 })
             });
 
@@ -938,6 +952,7 @@ impl WitPackageDecoder<'_> {
             types: IndexMap::default(),
             functions: IndexMap::new(),
             package: None,
+            stability: Default::default(),
         };
 
         let owner = TypeOwner::Interface(self.resolve.interfaces.next_id());
@@ -1032,6 +1047,7 @@ impl WitPackageDecoder<'_> {
             name: Some(name.to_string()),
             kind,
             docs: Default::default(),
+            stability: Default::default(),
             owner,
         });
 
@@ -1069,6 +1085,7 @@ impl WitPackageDecoder<'_> {
             includes: Default::default(),
             include_names: Default::default(),
             package: None,
+            stability: Default::default(),
         };
 
         let owner = TypeOwner::World(self.resolve.worlds.next_id());
@@ -1089,7 +1106,13 @@ impl WitPackageDecoder<'_> {
                         // with no name.
                         self.register_interface(name, ty, package)?
                     };
-                    (name, WorldItem::Interface(id))
+                    (
+                        name,
+                        WorldItem::Interface {
+                            id,
+                            stability: Default::default(),
+                        },
+                    )
                 }
                 types::ComponentEntityType::Type {
                     created,
@@ -1125,7 +1148,13 @@ impl WitPackageDecoder<'_> {
                     } else {
                         self.register_interface(name, ty, package)?
                     };
-                    (name, WorldItem::Interface(id))
+                    (
+                        name,
+                        WorldItem::Interface {
+                            id,
+                            stability: Default::default(),
+                        },
+                    )
                 }
 
                 types::ComponentEntityType::Func(idx) => {
@@ -1178,6 +1207,7 @@ impl WitPackageDecoder<'_> {
         };
         Ok(Function {
             docs: Default::default(),
+            stability: Default::default(),
             kind: match name.kind() {
                 ComponentNameKind::Label(_) => FunctionKind::Freestanding,
                 ComponentNameKind::Constructor(resource) => {
@@ -1247,6 +1277,7 @@ impl WitPackageDecoder<'_> {
         let ty = self.resolve.types.alloc(TypeDef {
             name: None,
             docs: Default::default(),
+            stability: Default::default(),
             owner: TypeOwner::None,
             kind,
         });
@@ -1480,8 +1511,8 @@ impl WitPackageDecoder<'_> {
             world.package = Some(pkg);
             for (name, item) in world.imports.iter().chain(world.exports.iter()) {
                 if let WorldKey::Name(_) = name {
-                    if let WorldItem::Interface(iface) = item {
-                        self.resolve.interfaces[*iface].package = Some(pkg);
+                    if let WorldItem::Interface { id, .. } = item {
+                        self.resolve.interfaces[*id].package = Some(pkg);
                     }
                 }
             }
@@ -1504,7 +1535,7 @@ impl WitPackageDecoder<'_> {
                     world.imports.values().chain(world.exports.values())
                 })
                 .filter_map(|item| match item {
-                    WorldItem::Interface(id) => Some(*id),
+                    WorldItem::Interface { id, .. } => Some(*id),
                     WorldItem::Function(_) | WorldItem::Type(_) => None,
                 }),
         );
diff --git a/crates/wit-parser/src/docs.rs b/crates/wit-parser/src/docs.rs
deleted file mode 100644
index a21ba185..00000000
--- a/crates/wit-parser/src/docs.rs
+++ /dev/null
@@ -1,389 +0,0 @@
-use crate::{
-    Docs, InterfaceId, PackageId, Resolve, TypeDefKind, TypeId, WorldId, WorldItem, WorldKey,
-};
-use anyhow::{bail, Result};
-use indexmap::IndexMap;
-#[cfg(feature = "serde")]
-use serde_derive::{Deserialize, Serialize};
-
-type StringMap<V> = IndexMap<String, V>;
-
-#[cfg(feature = "serde")]
-const PACKAGE_DOCS_SECTION_VERSION: u8 = 0;
-
-/// Represents serializable doc comments parsed from a WIT package.
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
-pub struct PackageDocs {
-    #[cfg_attr(
-        feature = "serde",
-        serde(default, skip_serializing_if = "Option::is_none")
-    )]
-    docs: Option<String>,
-    #[cfg_attr(
-        feature = "serde",
-        serde(default, skip_serializing_if = "StringMap::is_empty")
-    )]
-    worlds: StringMap<WorldDocs>,
-    #[cfg_attr(
-        feature = "serde",
-        serde(default, skip_serializing_if = "StringMap::is_empty")
-    )]
-    interfaces: StringMap<InterfaceDocs>,
-}
-
-impl PackageDocs {
-    pub const SECTION_NAME: &'static str = "package-docs";
-
-    /// Extract package docs for the given package.
-    pub fn extract(resolve: &Resolve, package: PackageId) -> Self {
-        let package = &resolve.packages[package];
-
-        let worlds = package
-            .worlds
-            .iter()
-            .map(|(name, id)| (name.to_string(), WorldDocs::extract(resolve, *id)))
-            .filter(|(_, item)| !item.is_empty())
-            .collect();
-        let interfaces = package
-            .interfaces
-            .iter()
-            .map(|(name, id)| (name.to_string(), InterfaceDocs::extract(resolve, *id)))
-            .filter(|(_, item)| !item.is_empty())
-            .collect();
-
-        Self {
-            docs: package.docs.contents.as_deref().map(Into::into),
-            worlds,
-            interfaces,
-        }
-    }
-
-    /// Inject package docs for the given package.
-    ///
-    /// This will override any existing docs in the [`Resolve`].
-    pub fn inject(&self, resolve: &mut Resolve, package: PackageId) -> Result<()> {
-        for (name, docs) in &self.worlds {
-            let Some(&id) = resolve.packages[package].worlds.get(name) else {
-                bail!("missing world {name:?}");
-            };
-            docs.inject(resolve, id)?;
-        }
-        for (name, docs) in &self.interfaces {
-            let Some(&id) = resolve.packages[package].interfaces.get(name) else {
-                bail!("missing interface {name:?}");
-            };
-            docs.inject(resolve, id)?;
-        }
-        if let Some(docs) = &self.docs {
-            resolve.packages[package].docs.contents = Some(docs.to_string());
-        }
-        Ok(())
-    }
-
-    /// Encode package docs as a package-docs custom section.
-    #[cfg(feature = "serde")]
-    pub fn encode(&self) -> Result<Vec<u8>> {
-        // Version byte (0), followed by JSON encoding of docs
-        let mut data = vec![PACKAGE_DOCS_SECTION_VERSION];
-        serde_json::to_writer(&mut data, self)?;
-        Ok(data)
-    }
-
-    /// Decode package docs from package-docs custom section content.
-    #[cfg(feature = "serde")]
-    pub fn decode(data: &[u8]) -> Result<Self> {
-        let version = data.first();
-        if version != Some(&PACKAGE_DOCS_SECTION_VERSION) {
-            bail!("expected package-docs version {PACKAGE_DOCS_SECTION_VERSION}, got {version:?}");
-        }
-        Ok(serde_json::from_slice(&data[1..])?)
-    }
-}
-
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
-struct WorldDocs {
-    #[cfg_attr(
-        feature = "serde",
-        serde(default, skip_serializing_if = "Option::is_none")
-    )]
-    docs: Option<String>,
-    #[cfg_attr(
-        feature = "serde",
-        serde(default, skip_serializing_if = "StringMap::is_empty")
-    )]
-    interfaces: StringMap<InterfaceDocs>,
-    #[cfg_attr(
-        feature = "serde",
-        serde(default, skip_serializing_if = "StringMap::is_empty")
-    )]
-    types: StringMap<TypeDocs>,
-    #[cfg_attr(
-        feature = "serde",
-        serde(default, skip_serializing_if = "StringMap::is_empty")
-    )]
-    funcs: StringMap<String>,
-}
-
-impl WorldDocs {
-    fn extract(resolve: &Resolve, id: WorldId) -> Self {
-        let world = &resolve.worlds[id];
-
-        let mut interfaces = StringMap::default();
-        let mut types = StringMap::default();
-        let mut funcs = StringMap::default();
-
-        // Iterate over all imports and exports, extracting documented items
-        for (key, item) in world.imports.iter().chain(&world.exports) {
-            if let WorldKey::Name(name) = key {
-                match item {
-                    WorldItem::Interface(id) => {
-                        let docs = InterfaceDocs::extract(resolve, *id);
-                        if !docs.is_empty() {
-                            interfaces.insert(name.to_string(), docs);
-                        }
-                    }
-                    WorldItem::Type(id) => {
-                        let docs = TypeDocs::extract(resolve, *id);
-                        if !docs.is_empty() {
-                            types.insert(name.to_string(), docs);
-                        }
-                    }
-                    WorldItem::Function(f) => {
-                        if let Some(docs) = f.docs.contents.as_deref() {
-                            funcs.insert(name.to_string(), docs.to_string());
-                        }
-                    }
-                }
-            }
-        }
-
-        Self {
-            docs: world.docs.contents.clone(),
-            interfaces,
-            types,
-            funcs,
-        }
-    }
-
-    fn inject(&self, resolve: &mut Resolve, id: WorldId) -> Result<()> {
-        for (name, docs) in &self.interfaces {
-            let key = WorldKey::Name(name.to_string());
-            let Some(WorldItem::Interface(id)) = resolve.worlds[id]
-                .imports
-                .get(&key)
-                .or_else(|| resolve.worlds[id].exports.get(&key))
-            else {
-                bail!("missing interface {name:?}");
-            };
-            docs.inject(resolve, *id)?;
-        }
-        for (name, docs) in &self.types {
-            let key = WorldKey::Name(name.to_string());
-            let Some(WorldItem::Type(id)) = resolve.worlds[id]
-                .imports
-                .get(&key)
-                .or_else(|| resolve.worlds[id].exports.get(&key))
-            else {
-                bail!("missing type {name:?}");
-            };
-            docs.inject(resolve, *id)?;
-        }
-        let world = &mut resolve.worlds[id];
-        for (name, docs) in &self.funcs {
-            let key = WorldKey::Name(name.to_string());
-            if let Some(WorldItem::Function(f)) = world.exports.get_mut(&key) {
-                f.docs.contents = Some(docs.to_string())
-            } else if let Some(WorldItem::Function(f)) = world.imports.get_mut(&key) {
-                f.docs.contents = Some(docs.to_string())
-            } else {
-                bail!("missing func {name:?}");
-            };
-        }
-        if let Some(docs) = &self.docs {
-            world.docs.contents = Some(docs.to_string());
-        }
-        Ok(())
-    }
-
-    fn is_empty(&self) -> bool {
-        self.docs.is_none()
-            && self.interfaces.is_empty()
-            && self.types.is_empty()
-            && self.funcs.is_empty()
-    }
-}
-
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
-struct InterfaceDocs {
-    #[cfg_attr(
-        feature = "serde",
-        serde(default, skip_serializing_if = "Option::is_none")
-    )]
-    docs: Option<String>,
-    #[cfg_attr(
-        feature = "serde",
-        serde(default, skip_serializing_if = "StringMap::is_empty")
-    )]
-    funcs: StringMap<String>,
-    #[cfg_attr(
-        feature = "serde",
-        serde(default, skip_serializing_if = "StringMap::is_empty")
-    )]
-    types: StringMap<TypeDocs>,
-}
-
-impl InterfaceDocs {
-    fn extract(resolve: &Resolve, id: InterfaceId) -> Self {
-        let interface = &resolve.interfaces[id];
-
-        let funcs = interface
-            .functions
-            .iter()
-            .flat_map(|(name, func)| Some((name.to_string(), func.docs.contents.clone()?)))
-            .collect();
-        let types = interface
-            .types
-            .iter()
-            .map(|(name, id)| (name.to_string(), TypeDocs::extract(resolve, *id)))
-            .filter(|(_, item)| !item.is_empty())
-            .collect();
-
-        Self {
-            docs: interface.docs.contents.clone(),
-            funcs,
-            types,
-        }
-    }
-
-    fn inject(&self, resolve: &mut Resolve, id: InterfaceId) -> Result<()> {
-        for (name, docs) in &self.types {
-            let Some(&id) = resolve.interfaces[id].types.get(name) else {
-                bail!("missing type {name:?}");
-            };
-            docs.inject(resolve, id)?;
-        }
-        let interface = &mut resolve.interfaces[id];
-        for (name, docs) in &self.funcs {
-            let Some(f) = interface.functions.get_mut(name) else {
-                bail!("missing func {name:?}");
-            };
-            f.docs.contents = Some(docs.to_string());
-        }
-        if let Some(docs) = &self.docs {
-            interface.docs.contents = Some(docs.to_string());
-        }
-        Ok(())
-    }
-
-    fn is_empty(&self) -> bool {
-        self.docs.is_none() && self.funcs.is_empty() && self.types.is_empty()
-    }
-}
-
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
-struct TypeDocs {
-    #[cfg_attr(
-        feature = "serde",
-        serde(default, skip_serializing_if = "Option::is_none")
-    )]
-    docs: Option<String>,
-    // record fields, variant cases, etc.
-    #[cfg_attr(
-        feature = "serde",
-        serde(default, skip_serializing_if = "StringMap::is_empty")
-    )]
-    items: StringMap<String>,
-}
-
-impl TypeDocs {
-    fn extract(resolve: &Resolve, id: TypeId) -> Self {
-        fn extract_items<T>(items: &[T], f: impl Fn(&T) -> (&String, &Docs)) -> StringMap<String> {
-            items
-                .iter()
-                .flat_map(|item| {
-                    let (name, docs) = f(item);
-                    Some((name.to_string(), docs.contents.clone()?))
-                })
-                .collect()
-        }
-        let ty = &resolve.types[id];
-        let items = match &ty.kind {
-            TypeDefKind::Record(record) => {
-                extract_items(&record.fields, |item| (&item.name, &item.docs))
-            }
-            TypeDefKind::Flags(flags) => {
-                extract_items(&flags.flags, |item| (&item.name, &item.docs))
-            }
-            TypeDefKind::Variant(variant) => {
-                extract_items(&variant.cases, |item| (&item.name, &item.docs))
-            }
-            TypeDefKind::Enum(enum_) => {
-                extract_items(&enum_.cases, |item| (&item.name, &item.docs))
-            }
-            // other types don't have inner items
-            _ => IndexMap::default(),
-        };
-
-        Self {
-            docs: ty.docs.contents.clone(),
-            items,
-        }
-    }
-
-    fn inject(&self, resolve: &mut Resolve, id: TypeId) -> Result<()> {
-        let ty = &mut resolve.types[id];
-        if !self.items.is_empty() {
-            match &mut ty.kind {
-                TypeDefKind::Record(record) => {
-                    self.inject_items(&mut record.fields, |item| (&item.name, &mut item.docs))?
-                }
-                TypeDefKind::Flags(flags) => {
-                    self.inject_items(&mut flags.flags, |item| (&item.name, &mut item.docs))?
-                }
-                TypeDefKind::Variant(variant) => {
-                    self.inject_items(&mut variant.cases, |item| (&item.name, &mut item.docs))?
-                }
-                TypeDefKind::Enum(enum_) => {
-                    self.inject_items(&mut enum_.cases, |item| (&item.name, &mut item.docs))?
-                }
-                _ => {
-                    bail!("got 'items' for unexpected type {ty:?}");
-                }
-            }
-        }
-        if let Some(docs) = &self.docs {
-            ty.docs.contents = Some(docs.to_string());
-        }
-        Ok(())
-    }
-
-    fn inject_items<T: std::fmt::Debug>(
-        &self,
-        items: &mut [T],
-        f: impl Fn(&mut T) -> (&String, &mut Docs),
-    ) -> Result<()> {
-        let mut unused_docs = self.items.len();
-        for item in items.iter_mut() {
-            let (name, item_docs) = f(item);
-            if let Some(docs) = self.items.get(name.as_str()) {
-                item_docs.contents = Some(docs.to_string());
-                unused_docs -= 1;
-            }
-        }
-        if unused_docs > 0 {
-            bail!(
-                "not all 'items' match type items; {item_docs:?} vs {items:?}",
-                item_docs = self.items
-            );
-        }
-        Ok(())
-    }
-
-    fn is_empty(&self) -> bool {
-        self.docs.is_none() && self.items.is_empty()
-    }
-}
diff --git a/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs
index 25700fb3..a9f54bd1 100644
--- a/crates/wit-parser/src/lib.rs
+++ b/crates/wit-parser/src/lib.rs
@@ -9,9 +9,9 @@ use std::path::Path;
 #[cfg(feature = "decoding")]
 pub mod decoding;
 #[cfg(feature = "decoding")]
-mod docs;
+mod metadata;
 #[cfg(feature = "decoding")]
-pub use docs::PackageDocs;
+pub use metadata::PackageMetadata;
 
 pub mod abi;
 mod ast;
@@ -29,10 +29,7 @@ use serde_derive::Serialize;
 #[cfg(feature = "serde")]
 mod serde_;
 #[cfg(feature = "serde")]
-use serde_::{
-    serialize_anon_result, serialize_id, serialize_id_map, serialize_none, serialize_optional_id,
-    serialize_params,
-};
+use serde_::*;
 
 /// Checks if the given string is a legal identifier in wit.
 pub fn validate_id(s: &str) -> Result<()> {
@@ -111,6 +108,7 @@ pub struct UnresolvedPackage {
     unknown_type_spans: Vec<Span>,
     interface_spans: Vec<InterfaceSpan>,
     world_spans: Vec<WorldSpan>,
+    type_spans: Vec<Span>,
     foreign_dep_spans: Vec<Span>,
     source_map: SourceMap,
     required_resource_types: Vec<(TypeId, Span)>,
@@ -190,11 +188,22 @@ impl fmt::Display for PackageName {
 struct Error {
     span: Span,
     msg: String,
+    highlighted: Option<String>,
+}
+
+impl Error {
+    fn new(span: Span, msg: impl Into<String>) -> Error {
+        Error {
+            span,
+            msg: msg.into(),
+            highlighted: None,
+        }
+    }
 }
 
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        self.msg.fmt(f)
+        self.highlighted.as_ref().unwrap_or(&self.msg).fmt(f)
     }
 }
 
@@ -292,9 +301,16 @@ pub struct World {
     #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Docs::is_empty"))]
     pub docs: Docs,
 
+    /// Stability annotation for this world itself.
+    #[cfg_attr(
+        feature = "serde",
+        serde(skip_serializing_if = "Stability::is_unknown")
+    )]
+    pub stability: Stability,
+
     /// All the included worlds from this world. Empty if this is fully resolved
     #[cfg_attr(feature = "serde", serde(skip))]
-    pub includes: Vec<WorldId>,
+    pub includes: Vec<(Stability, WorldId)>,
 
     /// All the included worlds names. Empty if this is fully resolved
     #[cfg_attr(feature = "serde", serde(skip))]
@@ -348,8 +364,15 @@ impl WorldKey {
 pub enum WorldItem {
     /// An interface is being imported or exported from a world, indicating that
     /// it's a namespace of functions.
-    #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))]
-    Interface(InterfaceId),
+    Interface {
+        #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_id"))]
+        id: InterfaceId,
+        #[cfg_attr(
+            feature = "serde",
+            serde(skip_serializing_if = "Stability::is_unknown")
+        )]
+        stability: Stability,
+    },
 
     /// A function is being directly imported or exported from this world.
     Function(Function),
@@ -361,6 +384,16 @@ pub enum WorldItem {
     Type(TypeId),
 }
 
+impl WorldItem {
+    pub fn stability<'a>(&'a self, resolve: &'a Resolve) -> &'a Stability {
+        match self {
+            WorldItem::Interface { stability, .. } => stability,
+            WorldItem::Function(f) => &f.stability,
+            WorldItem::Type(id) => &resolve.types[*id].stability,
+        }
+    }
+}
+
 #[derive(Debug, Clone)]
 #[cfg_attr(feature = "serde", derive(Serialize))]
 pub struct Interface {
@@ -383,6 +416,13 @@ pub struct Interface {
     #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Docs::is_empty"))]
     pub docs: Docs,
 
+    /// Stability attribute for this interface.
+    #[cfg_attr(
+        feature = "serde",
+        serde(skip_serializing_if = "Stability::is_unknown")
+    )]
+    pub stability: Stability,
+
     /// The package that owns this interface.
     #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_optional_id"))]
     pub package: Option<PackageId>,
@@ -396,6 +436,12 @@ pub struct TypeDef {
     pub owner: TypeOwner,
     #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Docs::is_empty"))]
     pub docs: Docs,
+    /// Stability attribute for this type.
+    #[cfg_attr(
+        feature = "serde",
+        serde(skip_serializing_if = "Stability::is_unknown")
+    )]
+    pub stability: Stability,
 }
 
 #[derive(Debug, Clone, PartialEq)]
@@ -724,6 +770,12 @@ pub struct Function {
     pub results: Results,
     #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Docs::is_empty"))]
     pub docs: Docs,
+    /// Stability attribute for this function.
+    #[cfg_attr(
+        feature = "serde",
+        serde(skip_serializing_if = "Stability::is_unknown")
+    )]
+    pub stability: Stability,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -759,6 +811,50 @@ impl Function {
     }
 }
 
+/// Representation of the stability attributes associated with a world,
+/// interface, function, or type.
+///
+/// This is added for WebAssembly/component-model#332 where @since and @unstable
+/// annotations were added to WIT.
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(serde_derive::Deserialize, Serialize))]
+#[cfg_attr(feature = "serde", serde(rename_all = "lowercase", tag = "type"))]
+pub enum Stability {
+    /// `@since(version = 1.2.3)`
+    ///
+    /// This item is explicitly tagged with `@since` as stable since the
+    /// specified version.  This may optionally have a feature listed as well.
+    Stable {
+        #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_version"))]
+        #[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_version"))]
+        since: Version,
+        #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
+        feature: Option<String>,
+    },
+
+    /// `@unstable(feature = foo)`
+    ///
+    /// This item is explicitly tagged `@unstable`. A feature name is listed and
+    /// this item is excluded by default in `Resolve` unless explicitly enabled.
+    Unstable { feature: String },
+
+    /// This item does not have either `@since` or `@unstable`.
+    Unknown,
+}
+
+impl Stability {
+    /// Returns whether this is `Stability::Unknown`.
+    pub fn is_unknown(&self) -> bool {
+        matches!(self, Stability::Unknown)
+    }
+}
+
+impl Default for Stability {
+    fn default() -> Stability {
+        Stability::Unknown
+    }
+}
+
 #[cfg(test)]
 mod test {
     use super::*;
diff --git a/crates/wit-parser/src/live.rs b/crates/wit-parser/src/live.rs
index ee9b7855..1b6986c8 100644
--- a/crates/wit-parser/src/live.rs
+++ b/crates/wit-parser/src/live.rs
@@ -36,7 +36,7 @@ impl LiveTypes {
 
     pub fn add_world_item(&mut self, resolve: &Resolve, item: &WorldItem) {
         match item {
-            WorldItem::Interface(id) => self.add_interface(resolve, *id),
+            WorldItem::Interface { id, .. } => self.add_interface(resolve, *id),
             WorldItem::Function(f) => self.add_func(resolve, f),
             WorldItem::Type(t) => self.add_type_id(resolve, *t),
         }
diff --git a/crates/wit-parser/src/metadata.rs b/crates/wit-parser/src/metadata.rs
new file mode 100644
index 00000000..103e3d03
--- /dev/null
+++ b/crates/wit-parser/src/metadata.rs
@@ -0,0 +1,772 @@
+//! Implementation of encoding/decoding package metadata (docs/stability) in a
+//! custom section.
+//!
+//! This module contains the particulars for how this custom section is encoded
+//! and decoded at this time. As of the time of this writing the component model
+//! binary format does not have any means of storing documentation and/or item
+//! stability inline with items themselves. These are important to preserve when
+//! round-tripping WIT through the WebAssembly binary format, however, so this
+//! module implements this with a custom section.
+//!
+//! The custom section, named `SECTION_NAME`, is stored within the component
+//! that encodes a WIT package. This section is itself JSON-encoded with a small
+//! version header to help forwards/backwards compatibility. The hope is that
+//! one day this custom section will be obsoleted by extensions to the binary
+//! format to store this information inline.
+
+use crate::{
+    Docs, Function, InterfaceId, PackageId, Resolve, Stability, TypeDefKind, TypeId, WorldId,
+    WorldItem, WorldKey,
+};
+use anyhow::{bail, Result};
+use indexmap::IndexMap;
+#[cfg(feature = "serde")]
+use serde_derive::{Deserialize, Serialize};
+
+type StringMap<V> = IndexMap<String, V>;
+
+/// Current supported format of the custom section.
+///
+/// This byte is a prefix byte intended to be a general version marker for the
+/// entire custom section. This is bumped when backwards-incompatible changes
+/// are made to prevent older implementations from loading newer versions.
+///
+/// The history of this is:
+///
+/// * [????/??/??] 0 - the original format added
+/// * [2024/04/19] 1 - extensions were added for item stability and
+///   additionally having world imports/exports have the same name.
+#[cfg(feature = "serde")]
+const PACKAGE_DOCS_SECTION_VERSION: u8 = 1;
+
+/// At this time the v1 format was just written. For compatibility with older
+/// tools we'll still try to emit the v0 format by default, if the input is
+/// compatible. This will be turned off in the future once enough published
+/// versions support the v1 format.
+const TRY_TO_EMIT_V0_BY_DEFAULT: bool = true;
+
+/// Represents serializable doc comments parsed from a WIT package.
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
+pub struct PackageMetadata {
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "Option::is_none")
+    )]
+    docs: Option<String>,
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "StringMap::is_empty")
+    )]
+    worlds: StringMap<WorldMetadata>,
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "StringMap::is_empty")
+    )]
+    interfaces: StringMap<InterfaceMetadata>,
+}
+
+impl PackageMetadata {
+    pub const SECTION_NAME: &'static str = "package-docs";
+
+    /// Extract package docs for the given package.
+    pub fn extract(resolve: &Resolve, package: PackageId) -> Self {
+        let package = &resolve.packages[package];
+
+        let worlds = package
+            .worlds
+            .iter()
+            .map(|(name, id)| (name.to_string(), WorldMetadata::extract(resolve, *id)))
+            .filter(|(_, item)| !item.is_empty())
+            .collect();
+        let interfaces = package
+            .interfaces
+            .iter()
+            .map(|(name, id)| (name.to_string(), InterfaceMetadata::extract(resolve, *id)))
+            .filter(|(_, item)| !item.is_empty())
+            .collect();
+
+        Self {
+            docs: package.docs.contents.as_deref().map(Into::into),
+            worlds,
+            interfaces,
+        }
+    }
+
+    /// Inject package docs for the given package.
+    ///
+    /// This will override any existing docs in the [`Resolve`].
+    pub fn inject(&self, resolve: &mut Resolve, package: PackageId) -> Result<()> {
+        for (name, docs) in &self.worlds {
+            let Some(&id) = resolve.packages[package].worlds.get(name) else {
+                bail!("missing world {name:?}");
+            };
+            docs.inject(resolve, id)?;
+        }
+        for (name, docs) in &self.interfaces {
+            let Some(&id) = resolve.packages[package].interfaces.get(name) else {
+                bail!("missing interface {name:?}");
+            };
+            docs.inject(resolve, id)?;
+        }
+        if let Some(docs) = &self.docs {
+            resolve.packages[package].docs.contents = Some(docs.to_string());
+        }
+        Ok(())
+    }
+
+    /// Encode package docs as a package-docs custom section.
+    #[cfg(feature = "serde")]
+    pub fn encode(&self) -> Result<Vec<u8>> {
+        // Version byte, followed by JSON encoding of docs.
+        //
+        // Note that if this document is compatible with the v0 format then
+        // that's preferred to keep older tools working at this time.
+        // Eventually this branch will be removed and v1 will unconditionally
+        // be used.
+        let mut data = vec![
+            if TRY_TO_EMIT_V0_BY_DEFAULT && self.is_compatible_with_v0() {
+                0
+            } else {
+                PACKAGE_DOCS_SECTION_VERSION
+            },
+        ];
+        serde_json::to_writer(&mut data, self)?;
+        Ok(data)
+    }
+
+    /// Decode package docs from package-docs custom section content.
+    #[cfg(feature = "serde")]
+    pub fn decode(data: &[u8]) -> Result<Self> {
+        match data.first().copied() {
+            // Our serde structures transparently support v0 and the current
+            // version, so allow either here.
+            Some(0) | Some(PACKAGE_DOCS_SECTION_VERSION) => {}
+            version => {
+                bail!(
+                    "expected package-docs version {PACKAGE_DOCS_SECTION_VERSION}, got {version:?}"
+                );
+            }
+        }
+        Ok(serde_json::from_slice(&data[1..])?)
+    }
+
+    #[cfg(feature = "serde")]
+    fn is_compatible_with_v0(&self) -> bool {
+        self.worlds.iter().all(|(_, w)| w.is_compatible_with_v0())
+            && self
+                .interfaces
+                .iter()
+                .all(|(_, w)| w.is_compatible_with_v0())
+    }
+}
+
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
+struct WorldMetadata {
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "Option::is_none")
+    )]
+    docs: Option<String>,
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "Stability::is_unknown")
+    )]
+    stability: Stability,
+
+    /// Metadata for named interface, e.g.:
+    ///
+    /// ```wit
+    /// world foo {
+    ///     import x: interface {}
+    /// }
+    /// ```
+    ///
+    /// In the v0 format this was called "interfaces", hence the
+    /// `serde(rename)`. When support was originally added here imports/exports
+    /// could not overlap in their name, but now they can. This map has thus
+    /// been repurposed as:
+    ///
+    /// * If an interface is imported, it goes here.
+    /// * If an interface is exported, and no interface was imported with the
+    ///   same name, it goes here.
+    ///
+    /// Otherwise exports go inside the `interface_exports` map.
+    ///
+    /// In the future when v0 support is dropped this should become only
+    /// imports, not either imports-or-exports.
+    #[cfg_attr(
+        feature = "serde",
+        serde(
+            default,
+            rename = "interfaces",
+            skip_serializing_if = "StringMap::is_empty"
+        )
+    )]
+    interface_imports_or_exports: StringMap<InterfaceMetadata>,
+
+    /// All types in this interface.
+    ///
+    /// Note that at this time types are only imported, never exported.
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "StringMap::is_empty")
+    )]
+    types: StringMap<TypeMetadata>,
+
+    /// Same as `interface_imports_or_exports`, but for functions.
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, rename = "funcs", skip_serializing_if = "StringMap::is_empty")
+    )]
+    func_imports_or_exports: StringMap<FunctionMetadata>,
+
+    /// The "export half" of `interface_imports_or_exports`.
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "StringMap::is_empty")
+    )]
+    interface_exports: StringMap<InterfaceMetadata>,
+
+    /// The "export half" of `func_imports_or_exports`.
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "StringMap::is_empty")
+    )]
+    func_exports: StringMap<FunctionMetadata>,
+
+    /// Stability annotations for interface imports that aren't inline, for
+    /// example:
+    ///
+    /// ```wit
+    /// world foo {
+    ///     @since(version = 1.0.0)
+    ///     import an-interface;
+    /// }
+    /// ```
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "StringMap::is_empty")
+    )]
+    interface_import_stability: StringMap<Stability>,
+
+    /// Same as `interface_import_stability`, but for exports.
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "StringMap::is_empty")
+    )]
+    interface_export_stability: StringMap<Stability>,
+}
+
+impl WorldMetadata {
+    fn extract(resolve: &Resolve, id: WorldId) -> Self {
+        let world = &resolve.worlds[id];
+
+        let mut interface_imports_or_exports = StringMap::default();
+        let mut types = StringMap::default();
+        let mut func_imports_or_exports = StringMap::default();
+        let mut interface_exports = StringMap::default();
+        let mut func_exports = StringMap::default();
+        let mut interface_import_stability = StringMap::default();
+        let mut interface_export_stability = StringMap::default();
+
+        for ((key, item), import) in world
+            .imports
+            .iter()
+            .map(|p| (p, true))
+            .chain(world.exports.iter().map(|p| (p, false)))
+        {
+            match key {
+                // For all named imports with kebab-names extract their
+                // docs/stability and insert it into one of our maps.
+                WorldKey::Name(name) => match item {
+                    WorldItem::Interface { id, .. } => {
+                        let data = InterfaceMetadata::extract(resolve, *id);
+                        if data.is_empty() {
+                            continue;
+                        }
+                        let map = if import {
+                            &mut interface_imports_or_exports
+                        } else if !TRY_TO_EMIT_V0_BY_DEFAULT
+                            || interface_imports_or_exports.contains_key(name)
+                        {
+                            &mut interface_exports
+                        } else {
+                            &mut interface_imports_or_exports
+                        };
+                        let prev = map.insert(name.to_string(), data);
+                        assert!(prev.is_none());
+                    }
+                    WorldItem::Type(id) => {
+                        let data = TypeMetadata::extract(resolve, *id);
+                        if !data.is_empty() {
+                            types.insert(name.to_string(), data);
+                        }
+                    }
+                    WorldItem::Function(f) => {
+                        let data = FunctionMetadata::extract(f);
+                        if data.is_empty() {
+                            continue;
+                        }
+                        let map = if import {
+                            &mut func_imports_or_exports
+                        } else if !TRY_TO_EMIT_V0_BY_DEFAULT
+                            || func_imports_or_exports.contains_key(name)
+                        {
+                            &mut func_exports
+                        } else {
+                            &mut func_imports_or_exports
+                        };
+                        let prev = map.insert(name.to_string(), data);
+                        assert!(prev.is_none());
+                    }
+                },
+
+                // For interface imports/exports extract the stability and
+                // record it if necessary.
+                WorldKey::Interface(_) => {
+                    let stability = match item {
+                        WorldItem::Interface { stability, .. } => stability,
+                        _ => continue,
+                    };
+                    if stability.is_unknown() {
+                        continue;
+                    }
+
+                    let map = if import {
+                        &mut interface_import_stability
+                    } else {
+                        &mut interface_export_stability
+                    };
+                    let name = resolve.name_world_key(key);
+                    map.insert(name, stability.clone());
+                }
+            }
+        }
+
+        Self {
+            docs: world.docs.contents.clone(),
+            stability: world.stability.clone(),
+            interface_imports_or_exports,
+            types,
+            func_imports_or_exports,
+            interface_exports,
+            func_exports,
+            interface_import_stability,
+            interface_export_stability,
+        }
+    }
+
+    fn inject(&self, resolve: &mut Resolve, id: WorldId) -> Result<()> {
+        // Inject docs/stability for all kebab-named interfaces, both imports
+        // and exports.
+        for ((name, data), only_export) in self
+            .interface_imports_or_exports
+            .iter()
+            .map(|p| (p, false))
+            .chain(self.interface_exports.iter().map(|p| (p, true)))
+        {
+            let key = WorldKey::Name(name.to_string());
+            let world = &mut resolve.worlds[id];
+
+            let item = if only_export {
+                world.exports.get_mut(&key)
+            } else {
+                match world.imports.get_mut(&key) {
+                    Some(item) => Some(item),
+                    None => world.exports.get_mut(&key),
+                }
+            };
+            let Some(WorldItem::Interface { id, stability }) = item else {
+                bail!("missing interface {name:?}");
+            };
+            *stability = data.stability.clone();
+            let id = *id;
+            data.inject(resolve, id)?;
+        }
+
+        // Process all types, which are always imported, for this world.
+        for (name, data) in &self.types {
+            let key = WorldKey::Name(name.to_string());
+            let Some(WorldItem::Type(id)) = resolve.worlds[id].imports.get(&key) else {
+                bail!("missing type {name:?}");
+            };
+            data.inject(resolve, *id)?;
+        }
+
+        // Build a map of `name_world_key` for interface imports/exports to the
+        // actual key. This map is then consluted in the next loop.
+        let world = &resolve.worlds[id];
+        let stabilities = world
+            .imports
+            .iter()
+            .map(|i| (i, true))
+            .chain(world.exports.iter().map(|i| (i, false)))
+            .filter_map(|((key, item), import)| match item {
+                WorldItem::Interface { .. } => {
+                    Some(((resolve.name_world_key(key), import), key.clone()))
+                }
+                _ => None,
+            })
+            .collect::<IndexMap<_, _>>();
+
+        let world = &mut resolve.worlds[id];
+
+        // Update the stability of an interface imports/exports that aren't
+        // kebab-named.
+        for ((name, stability), import) in self
+            .interface_import_stability
+            .iter()
+            .map(|p| (p, true))
+            .chain(self.interface_export_stability.iter().map(|p| (p, false)))
+        {
+            let key = match stabilities.get(&(name.clone(), import)) {
+                Some(key) => key.clone(),
+                None => bail!("missing interface `{name}`"),
+            };
+            let item = if import {
+                world.imports.get_mut(&key)
+            } else {
+                world.exports.get_mut(&key)
+            };
+            match item {
+                Some(WorldItem::Interface { stability: s, .. }) => *s = stability.clone(),
+                _ => bail!("item `{name}` wasn't an interface"),
+            }
+        }
+
+        // Update the docs/stability of all functions imported/exported from
+        // this world.
+        for ((name, data), only_export) in self
+            .func_imports_or_exports
+            .iter()
+            .map(|p| (p, false))
+            .chain(self.func_exports.iter().map(|p| (p, true)))
+        {
+            let key = WorldKey::Name(name.to_string());
+            let item = if only_export {
+                world.exports.get_mut(&key)
+            } else {
+                match world.imports.get_mut(&key) {
+                    Some(item) => Some(item),
+                    None => world.exports.get_mut(&key),
+                }
+            };
+            match item {
+                Some(WorldItem::Function(f)) => data.inject(f)?,
+                _ => bail!("missing func {name:?}"),
+            }
+        }
+        if let Some(docs) = &self.docs {
+            world.docs.contents = Some(docs.to_string());
+        }
+        world.stability = self.stability.clone();
+        Ok(())
+    }
+
+    fn is_empty(&self) -> bool {
+        self.docs.is_none()
+            && self.interface_imports_or_exports.is_empty()
+            && self.types.is_empty()
+            && self.func_imports_or_exports.is_empty()
+            && self.stability.is_unknown()
+            && self.interface_exports.is_empty()
+            && self.func_exports.is_empty()
+            && self.interface_import_stability.is_empty()
+            && self.interface_export_stability.is_empty()
+    }
+
+    #[cfg(feature = "serde")]
+    fn is_compatible_with_v0(&self) -> bool {
+        self.stability.is_unknown()
+            && self
+                .interface_imports_or_exports
+                .iter()
+                .all(|(_, w)| w.is_compatible_with_v0())
+            && self
+                .func_imports_or_exports
+                .iter()
+                .all(|(_, w)| w.is_compatible_with_v0())
+            && self.types.iter().all(|(_, w)| w.is_compatible_with_v0())
+            // These maps weren't present in v0, so we're only compatible if
+            // they're empty.
+            && self.interface_exports.is_empty()
+            && self.func_exports.is_empty()
+            && self.interface_import_stability.is_empty()
+            && self.interface_export_stability.is_empty()
+    }
+}
+
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
+struct InterfaceMetadata {
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "Option::is_none")
+    )]
+    docs: Option<String>,
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "Stability::is_unknown")
+    )]
+    stability: Stability,
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "StringMap::is_empty")
+    )]
+    funcs: StringMap<FunctionMetadata>,
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "StringMap::is_empty")
+    )]
+    types: StringMap<TypeMetadata>,
+}
+
+impl InterfaceMetadata {
+    fn extract(resolve: &Resolve, id: InterfaceId) -> Self {
+        let interface = &resolve.interfaces[id];
+
+        let funcs = interface
+            .functions
+            .iter()
+            .map(|(name, func)| (name.to_string(), FunctionMetadata::extract(func)))
+            .filter(|(_, item)| !item.is_empty())
+            .collect();
+        let types = interface
+            .types
+            .iter()
+            .map(|(name, id)| (name.to_string(), TypeMetadata::extract(resolve, *id)))
+            .filter(|(_, item)| !item.is_empty())
+            .collect();
+
+        Self {
+            docs: interface.docs.contents.clone(),
+            stability: interface.stability.clone(),
+            funcs,
+            types,
+        }
+    }
+
+    fn inject(&self, resolve: &mut Resolve, id: InterfaceId) -> Result<()> {
+        for (name, data) in &self.types {
+            let Some(&id) = resolve.interfaces[id].types.get(name) else {
+                bail!("missing type {name:?}");
+            };
+            data.inject(resolve, id)?;
+        }
+        let interface = &mut resolve.interfaces[id];
+        for (name, data) in &self.funcs {
+            let Some(f) = interface.functions.get_mut(name) else {
+                bail!("missing func {name:?}");
+            };
+            data.inject(f)?;
+        }
+        if let Some(docs) = &self.docs {
+            interface.docs.contents = Some(docs.to_string());
+        }
+        interface.stability = self.stability.clone();
+        Ok(())
+    }
+
+    fn is_empty(&self) -> bool {
+        self.docs.is_none()
+            && self.funcs.is_empty()
+            && self.types.is_empty()
+            && self.stability.is_unknown()
+    }
+
+    #[cfg(feature = "serde")]
+    fn is_compatible_with_v0(&self) -> bool {
+        self.stability.is_unknown()
+            && self.funcs.iter().all(|(_, w)| w.is_compatible_with_v0())
+            && self.types.iter().all(|(_, w)| w.is_compatible_with_v0())
+    }
+}
+
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(untagged, deny_unknown_fields))]
+enum FunctionMetadata {
+    /// In the v0 format function metadata was only a string so this variant
+    /// is preserved for the v0 format. In the future this can be removed
+    /// entirely in favor of just the below struct variant.
+    ///
+    /// Note that this is an untagged enum so the name `JustDocs` is just for
+    /// rust.
+    JustDocs(Option<String>),
+
+    /// In the v1+ format we're tracking at least docs but also the stability
+    /// of functions.
+    DocsAndStabilty {
+        #[cfg_attr(
+            feature = "serde",
+            serde(default, skip_serializing_if = "Option::is_none")
+        )]
+        docs: Option<String>,
+        #[cfg_attr(
+            feature = "serde",
+            serde(default, skip_serializing_if = "Stability::is_unknown")
+        )]
+        stability: Stability,
+    },
+}
+
+impl FunctionMetadata {
+    fn extract(func: &Function) -> Self {
+        if TRY_TO_EMIT_V0_BY_DEFAULT && func.stability.is_unknown() {
+            FunctionMetadata::JustDocs(func.docs.contents.clone())
+        } else {
+            FunctionMetadata::DocsAndStabilty {
+                docs: func.docs.contents.clone(),
+                stability: func.stability.clone(),
+            }
+        }
+    }
+
+    fn inject(&self, func: &mut Function) -> Result<()> {
+        match self {
+            FunctionMetadata::JustDocs(docs) => {
+                func.docs.contents = docs.clone();
+            }
+            FunctionMetadata::DocsAndStabilty { docs, stability } => {
+                func.docs.contents = docs.clone();
+                func.stability = stability.clone();
+            }
+        }
+        Ok(())
+    }
+
+    fn is_empty(&self) -> bool {
+        match self {
+            FunctionMetadata::JustDocs(docs) => docs.is_none(),
+            FunctionMetadata::DocsAndStabilty { docs, stability } => {
+                docs.is_none() && stability.is_unknown()
+            }
+        }
+    }
+
+    #[cfg(feature = "serde")]
+    fn is_compatible_with_v0(&self) -> bool {
+        match self {
+            FunctionMetadata::JustDocs(_) => true,
+            FunctionMetadata::DocsAndStabilty { .. } => false,
+        }
+    }
+}
+
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
+struct TypeMetadata {
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "Option::is_none")
+    )]
+    docs: Option<String>,
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "Stability::is_unknown")
+    )]
+    stability: Stability,
+    // record fields, variant cases, etc.
+    #[cfg_attr(
+        feature = "serde",
+        serde(default, skip_serializing_if = "StringMap::is_empty")
+    )]
+    items: StringMap<String>,
+}
+
+impl TypeMetadata {
+    fn extract(resolve: &Resolve, id: TypeId) -> Self {
+        fn extract_items<T>(items: &[T], f: impl Fn(&T) -> (&String, &Docs)) -> StringMap<String> {
+            items
+                .iter()
+                .flat_map(|item| {
+                    let (name, docs) = f(item);
+                    Some((name.to_string(), docs.contents.clone()?))
+                })
+                .collect()
+        }
+        let ty = &resolve.types[id];
+        let items = match &ty.kind {
+            TypeDefKind::Record(record) => {
+                extract_items(&record.fields, |item| (&item.name, &item.docs))
+            }
+            TypeDefKind::Flags(flags) => {
+                extract_items(&flags.flags, |item| (&item.name, &item.docs))
+            }
+            TypeDefKind::Variant(variant) => {
+                extract_items(&variant.cases, |item| (&item.name, &item.docs))
+            }
+            TypeDefKind::Enum(enum_) => {
+                extract_items(&enum_.cases, |item| (&item.name, &item.docs))
+            }
+            // other types don't have inner items
+            _ => IndexMap::default(),
+        };
+
+        Self {
+            docs: ty.docs.contents.clone(),
+            stability: ty.stability.clone(),
+            items,
+        }
+    }
+
+    fn inject(&self, resolve: &mut Resolve, id: TypeId) -> Result<()> {
+        let ty = &mut resolve.types[id];
+        if !self.items.is_empty() {
+            match &mut ty.kind {
+                TypeDefKind::Record(record) => {
+                    self.inject_items(&mut record.fields, |item| (&item.name, &mut item.docs))?
+                }
+                TypeDefKind::Flags(flags) => {
+                    self.inject_items(&mut flags.flags, |item| (&item.name, &mut item.docs))?
+                }
+                TypeDefKind::Variant(variant) => {
+                    self.inject_items(&mut variant.cases, |item| (&item.name, &mut item.docs))?
+                }
+                TypeDefKind::Enum(enum_) => {
+                    self.inject_items(&mut enum_.cases, |item| (&item.name, &mut item.docs))?
+                }
+                _ => {
+                    bail!("got 'items' for unexpected type {ty:?}");
+                }
+            }
+        }
+        if let Some(docs) = &self.docs {
+            ty.docs.contents = Some(docs.to_string());
+        }
+        ty.stability = self.stability.clone();
+        Ok(())
+    }
+
+    fn inject_items<T: std::fmt::Debug>(
+        &self,
+        items: &mut [T],
+        f: impl Fn(&mut T) -> (&String, &mut Docs),
+    ) -> Result<()> {
+        let mut unused_docs = self.items.len();
+        for item in items.iter_mut() {
+            let (name, item_docs) = f(item);
+            if let Some(docs) = self.items.get(name.as_str()) {
+                item_docs.contents = Some(docs.to_string());
+                unused_docs -= 1;
+            }
+        }
+        if unused_docs > 0 {
+            bail!(
+                "not all 'items' match type items; {item_docs:?} vs {items:?}",
+                item_docs = self.items
+            );
+        }
+        Ok(())
+    }
+
+    fn is_empty(&self) -> bool {
+        self.docs.is_none() && self.items.is_empty() && self.stability.is_unknown()
+    }
+
+    #[cfg(feature = "serde")]
+    fn is_compatible_with_v0(&self) -> bool {
+        self.stability.is_unknown()
+    }
+}
diff --git a/crates/wit-parser/src/resolve.rs b/crates/wit-parser/src/resolve.rs
index 19ccee6b..fa294f98 100644
--- a/crates/wit-parser/src/resolve.rs
+++ b/crates/wit-parser/src/resolve.rs
@@ -4,7 +4,7 @@ use crate::ast::{parse_use_path, AstUsePath};
 use crate::serde_::{serialize_arena, serialize_id_map};
 use crate::{
     AstItem, Docs, Error, Function, FunctionKind, Handle, IncludeName, Interface, InterfaceId,
-    InterfaceSpan, PackageName, Results, Type, TypeDef, TypeDefKind, TypeId, TypeOwner,
+    InterfaceSpan, PackageName, Results, Stability, Type, TypeDef, TypeDefKind, TypeId, TypeOwner,
     UnresolvedPackage, World, WorldId, WorldItem, WorldKey, WorldSpan,
 };
 use anyhow::{anyhow, bail, Context, Result};
@@ -64,6 +64,15 @@ pub struct Resolve {
     /// A map of package names to the ID of the package with that name.
     #[cfg_attr(feature = "serde", serde(skip))]
     pub package_names: IndexMap<PackageName, PackageId>,
+
+    /// Activated features for this [`Resolve`].
+    ///
+    /// This set of features is empty by default. This is consulted for
+    /// `@unstable` annotations in loaded WIT documents. Any items with
+    /// `@unstable` are filtered out unless their feature is present within this
+    /// set.
+    #[cfg_attr(feature = "serde", serde(skip))]
+    pub features: IndexSet<String>,
 }
 
 /// A WIT package within a `Resolve`.
@@ -205,10 +214,7 @@ impl Resolve {
                 for (i, (dep, _)) in pkg.foreign_deps.iter().enumerate() {
                     let span = pkg.foreign_dep_spans[i];
                     if !visiting.insert(dep) {
-                        bail!(Error {
-                            span,
-                            msg: format!("package depends on itself"),
-                        });
+                        bail!(Error::new(span, "package depends on itself"));
                     }
                     if let Some(dep) = deps.get(dep) {
                         visit(dep, deps, order, visiting)?;
@@ -447,18 +453,22 @@ impl Resolve {
             interfaces,
             packages,
             package_names,
+            features: _,
         } = resolve;
 
         let mut moved_types = Vec::new();
         for (id, mut ty) in types {
-            let new_id = type_map.get(&id).copied().unwrap_or_else(|| {
-                log::debug!("moving type {:?}", ty.name);
-                moved_types.push(id);
-                remap.update_typedef(self, &mut ty);
-                self.types.alloc(ty)
-            });
+            let new_id = match type_map.get(&id).copied() {
+                Some(id) => id,
+                None => {
+                    log::debug!("moving type {:?}", ty.name);
+                    moved_types.push(id);
+                    remap.update_typedef(self, &mut ty, None)?;
+                    self.types.alloc(ty)
+                }
+            };
             assert_eq!(remap.types.len(), id.index());
-            remap.types.push(new_id);
+            remap.types.push(Some(new_id));
         }
 
         let mut moved_interfaces = Vec::new();
@@ -473,7 +483,7 @@ impl Resolve {
                 }
             };
             assert_eq!(remap.interfaces.len(), id.index());
-            remap.interfaces.push(new_id);
+            remap.interfaces.push(Some(new_id));
         }
 
         let mut moved_worlds = Vec::new();
@@ -485,11 +495,13 @@ impl Resolve {
                     moved_worlds.push(id);
                     let mut update = |map: &mut IndexMap<WorldKey, WorldItem>| -> Result<_> {
                         for (mut name, mut item) in mem::take(map) {
-                            remap.update_world_key(&mut name);
+                            remap.update_world_key(&mut name, None)?;
                             match &mut item {
                                 WorldItem::Function(f) => remap.update_function(self, f, None)?,
-                                WorldItem::Interface(i) => *i = remap.interfaces[i.index()],
-                                WorldItem::Type(i) => *i = remap.types[i.index()],
+                                WorldItem::Interface { id, .. } => {
+                                    *id = remap.map_interface(*id, None)?
+                                }
+                                WorldItem::Type(i) => *i = remap.map_type(*i, None)?,
                             }
                             map.insert(name, item);
                         }
@@ -501,19 +513,22 @@ impl Resolve {
                 }
             };
             assert_eq!(remap.worlds.len(), id.index());
-            remap.worlds.push(new_id);
+            remap.worlds.push(Some(new_id));
         }
 
         for (id, mut pkg) in packages {
-            let new_id = package_map.get(&id).copied().unwrap_or_else(|| {
-                for (_, id) in pkg.interfaces.iter_mut() {
-                    *id = remap.interfaces[id.index()];
-                }
-                for (_, id) in pkg.worlds.iter_mut() {
-                    *id = remap.worlds[id.index()];
+            let new_id = match package_map.get(&id).copied() {
+                Some(id) => id,
+                None => {
+                    for (_, id) in pkg.interfaces.iter_mut() {
+                        *id = remap.map_interface(*id, None)?;
+                    }
+                    for (_, id) in pkg.worlds.iter_mut() {
+                        *id = remap.map_world(*id, None)?;
+                    }
+                    self.packages.alloc(pkg)
                 }
-                self.packages.alloc(pkg)
-            });
+            };
             assert_eq!(remap.packages.len(), id.index());
             remap.packages.push(new_id);
         }
@@ -533,20 +548,20 @@ impl Resolve {
         // are ids within `resolve`, so they're translated through `remap` to
         // ids within `self`.
         for id in moved_worlds {
-            let id = remap.worlds[id.index()];
+            let id = remap.map_world(id, None)?;
             let pkg = self.worlds[id].package.as_mut().unwrap();
             *pkg = remap.packages[pkg.index()];
         }
         for id in moved_interfaces {
-            let id = remap.interfaces[id.index()];
+            let id = remap.map_interface(id, None)?;
             let pkg = self.interfaces[id].package.as_mut().unwrap();
             *pkg = remap.packages[pkg.index()];
         }
         for id in moved_types {
-            let id = remap.types[id.index()];
+            let id = remap.map_type(id, None)?;
             match &mut self.types[id].owner {
-                TypeOwner::Interface(id) => *id = remap.interfaces[id.index()],
-                TypeOwner::World(id) => *id = remap.worlds[id.index()],
+                TypeOwner::Interface(id) => *id = remap.map_interface(*id, None)?,
+                TypeOwner::World(id) => *id = remap.map_world(*id, None)?,
                 TypeOwner::None => {}
             }
         }
@@ -558,13 +573,13 @@ impl Resolve {
         for (name, pkg, iface) in interfaces_to_add {
             let prev = self.packages[pkg]
                 .interfaces
-                .insert(name, remap.interfaces[iface.index()]);
+                .insert(name, remap.map_interface(iface, None)?);
             assert!(prev.is_none());
         }
         for (name, pkg, world) in worlds_to_add {
             let prev = self.packages[pkg]
                 .worlds
-                .insert(name, remap.worlds[world.index()]);
+                .insert(name, remap.map_world(world, None)?);
             assert!(prev.is_none());
         }
 
@@ -596,13 +611,13 @@ impl Resolve {
         let mut into_imports_by_id = HashMap::new();
         let mut into_exports_by_id = HashMap::new();
         for (name, import) in into_world.imports.iter() {
-            if let WorldItem::Interface(id) = *import {
+            if let WorldItem::Interface { id, .. } = *import {
                 let prev = into_imports_by_id.insert(id, name);
                 assert!(prev.is_none());
             }
         }
         for (name, export) in into_world.exports.iter() {
-            if let WorldItem::Interface(id) = *export {
+            if let WorldItem::Interface { id, .. } = *export {
                 let prev = into_exports_by_id.insert(id, name);
                 assert!(prev.is_none());
             }
@@ -613,7 +628,7 @@ impl Resolve {
             // are the same. Importing the same interface under different names
             // isn't allowed, but otherwise merging imports of
             // same-named-interfaces is allowed to merge them together.
-            if let WorldItem::Interface(id) = import {
+            if let WorldItem::Interface { id, .. } = import {
                 if let Some(prev) = into_imports_by_id.get(id) {
                     if *prev != name {
                         let name = self.name_world_key(name);
@@ -627,7 +642,7 @@ impl Resolve {
             // Note that unlike imports same-named exports are not handled here
             // since if something is exported twice there's no way to "unify" it
             // so it's left as an error.
-            if let WorldItem::Interface(id) = export {
+            if let WorldItem::Interface { id, .. } = export {
                 if let Some(prev) = into_exports_by_id.get(id) {
                     let name = self.name_world_key(name);
                     let prev = self.name_world_key(prev);
@@ -643,9 +658,10 @@ impl Resolve {
                 Some(into_import) => match (from_import, into_import) {
                     // If these imports, which have the same name, are of the
                     // same interface then union them together at this point.
-                    (WorldItem::Interface(from), WorldItem::Interface(into)) if from == into => {
-                        continue
-                    }
+                    (
+                        WorldItem::Interface { id: from, .. },
+                        WorldItem::Interface { id: into, .. },
+                    ) if from == into => continue,
                     _ => {
                         let name = self.name_world_key(name);
                         bail!("duplicate import found for interface `{name}`");
@@ -849,7 +865,7 @@ impl Resolve {
                     .iter()
                     .chain(world.exports.iter())
                     .filter_map(move |(_name, item)| match item {
-                        WorldItem::Interface(id) => Some(*id),
+                        WorldItem::Interface { id, .. } => Some(*id),
                         WorldItem::Function(_) => None,
                         WorldItem::Type(t) => self.type_interface_dep(*t),
                     })
@@ -945,7 +961,7 @@ impl Resolve {
             for (name, item) in world.imports.iter().chain(world.exports.iter()) {
                 log::debug!("validating world item: {}", self.name_world_key(name));
                 match item {
-                    WorldItem::Interface(_) => {}
+                    WorldItem::Interface { .. } => {}
                     WorldItem::Function(f) => {
                         assert_eq!(f.name, name.clone().unwrap_name());
                     }
@@ -1006,7 +1022,7 @@ impl Resolve {
 
             for (_, item) in world.imports.iter().chain(&world.exports) {
                 let id = match item {
-                    WorldItem::Interface(id) => *id,
+                    WorldItem::Interface { id, .. } => *id,
                     _ => continue,
                 };
                 let other_package = self.interfaces[id].package;
@@ -1050,15 +1066,22 @@ impl Resolve {
             }
         }
     }
+
+    fn include_stability(&self, stability: &Stability) -> bool {
+        match stability {
+            Stability::Stable { .. } | Stability::Unknown => true,
+            Stability::Unstable { feature } => self.features.contains(feature),
+        }
+    }
 }
 
 /// Structure returned by [`Resolve::merge`] which contains mappings from
 /// old-ids to new-ids after the merge.
 #[derive(Default)]
 pub struct Remap {
-    pub types: Vec<TypeId>,
-    pub interfaces: Vec<InterfaceId>,
-    pub worlds: Vec<WorldId>,
+    pub types: Vec<Option<TypeId>>,
+    pub interfaces: Vec<Option<InterfaceId>>,
+    pub worlds: Vec<Option<WorldId>>,
     pub packages: Vec<PackageId>,
 
     /// A cache of anonymous `own<T>` handles for resource types.
@@ -1075,7 +1098,36 @@ pub struct Remap {
     type_has_borrow: Vec<Option<bool>>,
 }
 
+fn apply_map<T>(map: &[Option<Id<T>>], id: Id<T>, desc: &str, span: Option<Span>) -> Result<Id<T>> {
+    match map.get(id.index()) {
+        Some(Some(id)) => Ok(*id),
+        Some(None) => {
+            let msg = format!(
+                "found a reference to a {desc} which is excluded \
+                 due to its feature not being activated"
+            );
+            match span {
+                Some(span) => Err(Error::new(span, msg).into()),
+                None => bail!("{msg}"),
+            }
+        }
+        None => panic!("request to remap a {desc} that has not yet been registered"),
+    }
+}
+
 impl Remap {
+    pub fn map_type(&self, id: TypeId, span: Option<Span>) -> Result<TypeId> {
+        apply_map(&self.types, id, "type", span)
+    }
+
+    pub fn map_interface(&self, id: InterfaceId, span: Option<Span>) -> Result<InterfaceId> {
+        apply_map(&self.interfaces, id, "interface", span)
+    }
+
+    pub fn map_world(&self, id: WorldId, span: Option<Span>) -> Result<WorldId> {
+        apply_map(&self.worlds, id, "world", span)
+    }
+
     fn append(
         &mut self,
         resolve: &mut Resolve,
@@ -1087,13 +1139,33 @@ impl Remap {
         let foreign_interfaces = self.interfaces.len();
         let foreign_worlds = self.worlds.len();
 
+        let pkgid = resolve.packages.alloc(Package {
+            name: unresolved.name.clone(),
+            docs: unresolved.docs.clone(),
+            interfaces: Default::default(),
+            worlds: Default::default(),
+        });
+        let prev = resolve.package_names.insert(unresolved.name.clone(), pkgid);
+        assert!(prev.is_none());
+
         // Copy over all types first, updating any intra-type references. Note
         // that types are sorted topologically which means this iteration
         // order should be sufficient. Also note though that the interface
         // owner of a type isn't updated here due to interfaces not being known
         // yet.
-        for (id, mut ty) in unresolved.types.into_iter().skip(foreign_types) {
-            self.update_typedef(resolve, &mut ty);
+        assert_eq!(unresolved.types.len(), unresolved.type_spans.len());
+        for ((id, mut ty), span) in unresolved
+            .types
+            .into_iter()
+            .zip(&unresolved.type_spans)
+            .skip(foreign_types)
+        {
+            if !resolve.include_stability(&ty.stability) {
+                self.types.push(None);
+                continue;
+            }
+
+            self.update_typedef(resolve, &mut ty, Some(*span))?;
             let new_id = resolve.types.alloc(ty);
             assert_eq!(self.types.len(), id.index());
 
@@ -1107,34 +1179,55 @@ impl Remap {
                     owner: TypeOwner::None,
                     kind: TypeDefKind::Handle(Handle::Own(id)),
                     docs: _,
+                    stability: _,
                 } => *self.own_handles.entry(id).or_insert(new_id),
 
                 // Everything not-related to `own<T>` doesn't get its ID
                 // modified.
                 _ => new_id,
             };
-            self.types.push(new_id);
+            self.types.push(Some(new_id));
         }
 
         // Next transfer all interfaces into `Resolve`, updating type ids
         // referenced along the way.
+        assert_eq!(
+            unresolved.interfaces.len(),
+            unresolved.interface_spans.len()
+        );
         for ((id, mut iface), span) in unresolved
             .interfaces
             .into_iter()
             .zip(&unresolved.interface_spans)
             .skip(foreign_interfaces)
         {
+            if !resolve.include_stability(&iface.stability) {
+                self.interfaces.push(None);
+                continue;
+            }
             self.update_interface(resolve, &mut iface, Some(span))?;
+            assert!(iface.package.is_none());
+            iface.package = Some(pkgid);
             let new_id = resolve.interfaces.alloc(iface);
             assert_eq!(self.interfaces.len(), id.index());
-            self.interfaces.push(new_id);
+            self.interfaces.push(Some(new_id));
         }
 
         // Now that interfaces are identified go back through the types and
         // update their interface owners.
-        for id in self.types.iter().skip(foreign_types) {
-            match &mut resolve.types[*id].owner {
-                TypeOwner::Interface(id) => *id = self.interfaces[id.index()],
+        for (i, id) in self.types.iter().enumerate().skip(foreign_types) {
+            let id = match id {
+                Some(id) => *id,
+                None => continue,
+            };
+            match &mut resolve.types[id].owner {
+                TypeOwner::Interface(id) => {
+                    let span = unresolved.type_spans[i];
+                    *id = self.map_interface(*id, Some(span))
+                        .with_context(|| {
+                            "this type is not gated by a feature but its interface is gated by a feature"
+                        })?;
+                }
                 TypeOwner::World(_) | TypeOwner::None => {}
             }
         }
@@ -1153,44 +1246,58 @@ impl Remap {
             .zip(unresolved.world_spans)
             .skip(foreign_worlds)
         {
+            if !resolve.include_stability(&world.stability) {
+                self.worlds.push(None);
+                continue;
+            }
             self.update_world(&mut world, resolve, &span)?;
 
             let new_id = resolve.worlds.alloc(world);
             assert_eq!(self.worlds.len(), id.index());
-            self.worlds.push(new_id);
+            self.worlds.push(Some(new_id));
         }
 
         // As with interfaces, now update the ids of world-owned types.
-        for id in self.types.iter().skip(foreign_types) {
-            match &mut resolve.types[*id].owner {
-                TypeOwner::World(id) => *id = self.worlds[id.index()],
+        for (i, id) in self.types.iter().enumerate().skip(foreign_types) {
+            let id = match id {
+                Some(id) => *id,
+                None => continue,
+            };
+            match &mut resolve.types[id].owner {
+                TypeOwner::World(id) => {
+                    let span = unresolved.type_spans[i];
+                    *id = self.map_world(*id, Some(span))
+                        .with_context(|| {
+                            "this type is not gated by a feature but its interface is gated by a feature"
+                        })?;
+                }
                 TypeOwner::Interface(_) | TypeOwner::None => {}
             }
         }
 
         // Fixup "parent" ids now that everything has been identified
-        let pkgid = resolve.packages.alloc(Package {
-            name: unresolved.name.clone(),
-            docs: unresolved.docs.clone(),
-            interfaces: Default::default(),
-            worlds: Default::default(),
-        });
-        let prev = resolve.package_names.insert(unresolved.name.clone(), pkgid);
-        assert!(prev.is_none());
         for id in self.interfaces.iter().skip(foreign_interfaces) {
-            let iface = &mut resolve.interfaces[*id];
+            let id = match id {
+                Some(id) => *id,
+                None => continue,
+            };
+            let iface = &mut resolve.interfaces[id];
             iface.package = Some(pkgid);
             if let Some(name) = &iface.name {
-                let prev = resolve.packages[pkgid].interfaces.insert(name.clone(), *id);
+                let prev = resolve.packages[pkgid].interfaces.insert(name.clone(), id);
                 assert!(prev.is_none());
             }
         }
         for id in self.worlds.iter().skip(foreign_worlds) {
-            let world = &mut resolve.worlds[*id];
+            let id = match id {
+                Some(id) => *id,
+                None => continue,
+            };
+            let world = &mut resolve.worlds[id];
             world.package = Some(pkgid);
             let prev = resolve.packages[pkgid]
                 .worlds
-                .insert(world.name.clone(), *id);
+                .insert(world.name.clone(), id);
             assert!(prev.is_none());
         }
         Ok(pkgid)
@@ -1241,15 +1348,15 @@ impl Remap {
         self.process_foreign_types(unresolved, resolve)?;
 
         for (id, span) in unresolved.required_resource_types.iter() {
-            let mut id = self.types[id.index()];
+            let mut id = self.map_type(*id, Some(*span))?;
             loop {
                 match resolve.types[id].kind {
                     TypeDefKind::Type(Type::Id(i)) => id = i,
                     TypeDefKind::Resource => break,
-                    _ => bail!(Error {
-                        span: *span,
-                        msg: format!("type used in a handle must be a resource"),
-                    }),
+                    _ => bail!(Error::new(
+                        *span,
+                        format!("type used in a handle must be a resource"),
+                    )),
                 }
             }
         }
@@ -1275,10 +1382,7 @@ impl Remap {
                 .package_names
                 .get(pkg_name)
                 .copied()
-                .ok_or_else(|| Error {
-                    span,
-                    msg: format!("package not found"),
-                })?;
+                .ok_or_else(|| Error::new(span, "package not found"))?;
 
             // Functions can't be imported so this should be empty.
             assert!(unresolved_iface.functions.is_empty());
@@ -1289,12 +1393,9 @@ impl Remap {
                 .interfaces
                 .get(interface)
                 .copied()
-                .ok_or_else(|| Error {
-                    span: span.span,
-                    msg: format!("interface not found in package"),
-                })?;
+                .ok_or_else(|| Error::new(span.span, "interface not found in package"))?;
             assert_eq!(self.interfaces.len(), unresolved_iface_id.index());
-            self.interfaces.push(iface_id);
+            self.interfaces.push(Some(iface_id));
         }
         for (id, _) in unresolved.interfaces.iter().skip(self.interfaces.len()) {
             assert!(
@@ -1323,18 +1424,16 @@ impl Remap {
                 .package_names
                 .get(pkg_name)
                 .copied()
-                .ok_or_else(|| Error {
-                    span,
-                    msg: format!("package not found"),
-                })?;
+                .ok_or_else(|| Error::new(span, "package not found"))?;
             let pkg = &resolve.packages[pkgid];
             let span = &unresolved.world_spans[unresolved_world_id.index()];
-            let world_id = pkg.worlds.get(world).copied().ok_or_else(|| Error {
-                span: span.span,
-                msg: format!("world not found in package"),
-            })?;
+            let world_id = pkg
+                .worlds
+                .get(world)
+                .copied()
+                .ok_or_else(|| Error::new(span.span, "world not found in package"))?;
             assert_eq!(self.worlds.len(), unresolved_world_id.index());
-            self.worlds.push(world_id);
+            self.worlds.push(Some(world_id));
         }
         for (id, _) in unresolved.worlds.iter().skip(self.worlds.len()) {
             assert!(
@@ -1362,18 +1461,17 @@ impl Remap {
                 TypeOwner::Interface(id) => id,
                 _ => unreachable!(),
             };
-            let iface_id = self.interfaces[unresolved_iface_id.index()];
+            let iface_id = self.map_interface(unresolved_iface_id, None)?;
             let name = unresolved_ty.name.as_ref().unwrap();
             let span = unresolved.unknown_type_spans[unresolved_type_id.index()];
             let type_id = *resolve.interfaces[iface_id]
                 .types
                 .get(name)
-                .ok_or_else(|| Error {
-                    span,
-                    msg: format!("type `{name}` not defined in interface"),
+                .ok_or_else(|| {
+                    Error::new(span, format!("type `{name}` not defined in interface"))
                 })?;
             assert_eq!(self.types.len(), unresolved_type_id.index());
-            self.types.push(type_id);
+            self.types.push(Some(type_id));
         }
         for (_, ty) in unresolved.types.iter().skip(self.types.len()) {
             if let TypeDefKind::Unknown = ty.kind {
@@ -1383,49 +1481,57 @@ impl Remap {
         Ok(())
     }
 
-    fn update_typedef(&mut self, resolve: &mut Resolve, ty: &mut TypeDef) {
+    fn update_typedef(
+        &mut self,
+        resolve: &mut Resolve,
+        ty: &mut TypeDef,
+        span: Option<Span>,
+    ) -> Result<()> {
         // NB: note that `ty.owner` is not updated here since interfaces
         // haven't been mapped yet and that's done in a separate step.
         use crate::TypeDefKind::*;
         match &mut ty.kind {
             Handle(handle) => match handle {
-                crate::Handle::Own(ty) | crate::Handle::Borrow(ty) => self.update_type_id(ty),
+                crate::Handle::Own(ty) | crate::Handle::Borrow(ty) => {
+                    self.update_type_id(ty, span)?
+                }
             },
             Resource => {}
             Record(r) => {
                 for field in r.fields.iter_mut() {
-                    self.update_ty(resolve, &mut field.ty);
+                    self.update_ty(resolve, &mut field.ty, span)
+                        .with_context(|| format!("failed to update field `{}`", field.name))?;
                 }
             }
             Tuple(t) => {
                 for ty in t.types.iter_mut() {
-                    self.update_ty(resolve, ty);
+                    self.update_ty(resolve, ty, span)?;
                 }
             }
             Variant(v) => {
                 for case in v.cases.iter_mut() {
                     if let Some(t) = &mut case.ty {
-                        self.update_ty(resolve, t);
+                        self.update_ty(resolve, t, span)?;
                     }
                 }
             }
-            Option(t) => self.update_ty(resolve, t),
+            Option(t) => self.update_ty(resolve, t, span)?,
             Result(r) => {
                 if let Some(ty) = &mut r.ok {
-                    self.update_ty(resolve, ty);
+                    self.update_ty(resolve, ty, span)?;
                 }
                 if let Some(ty) = &mut r.err {
-                    self.update_ty(resolve, ty);
+                    self.update_ty(resolve, ty, span)?;
                 }
             }
-            List(t) => self.update_ty(resolve, t),
-            Future(Some(t)) => self.update_ty(resolve, t),
+            List(t) => self.update_ty(resolve, t, span)?,
+            Future(Some(t)) => self.update_ty(resolve, t, span)?,
             Stream(t) => {
                 if let Some(ty) = &mut t.element {
-                    self.update_ty(resolve, ty);
+                    self.update_ty(resolve, ty, span)?;
                 }
                 if let Some(ty) = &mut t.end {
-                    self.update_ty(resolve, ty);
+                    self.update_ty(resolve, ty, span)?;
                 }
             }
 
@@ -1433,7 +1539,7 @@ impl Remap {
             // because for the `type a = b` form that doesn't force `a` to be a
             // handle type if `b` is a resource type, instead `a` is
             // simultaneously usable as a resource and a handle type
-            Type(crate::Type::Id(id)) => self.update_type_id(id),
+            Type(crate::Type::Id(id)) => self.update_type_id(id, span)?,
             Type(_) => {}
 
             // nothing to do for these as they're just names or empty
@@ -1441,14 +1547,21 @@ impl Remap {
 
             Unknown => unreachable!(),
         }
+
+        Ok(())
     }
 
-    fn update_ty(&mut self, resolve: &mut Resolve, ty: &mut Type) {
+    fn update_ty(
+        &mut self,
+        resolve: &mut Resolve,
+        ty: &mut Type,
+        span: Option<Span>,
+    ) -> Result<()> {
         let id = match ty {
             Type::Id(id) => id,
-            _ => return,
+            _ => return Ok(()),
         };
-        self.update_type_id(id);
+        self.update_type_id(id, span)?;
 
         // If `id` points to a `Resource` type then this means that what was
         // just discovered was a reference to what will implicitly become an
@@ -1470,13 +1583,16 @@ impl Remap {
                     owner: TypeOwner::None,
                     kind: TypeDefKind::Handle(Handle::Own(*id)),
                     docs: Default::default(),
+                    stability: Default::default(),
                 })
             });
         }
+        Ok(())
     }
 
-    fn update_type_id(&self, id: &mut TypeId) {
-        *id = self.types[id.index()];
+    fn update_type_id(&self, id: &mut TypeId, span: Option<Span>) -> Result<()> {
+        *id = self.map_type(*id, span)?;
+        Ok(())
     }
 
     fn update_interface(
@@ -1485,18 +1601,27 @@ impl Remap {
         iface: &mut Interface,
         spans: Option<&InterfaceSpan>,
     ) -> Result<()> {
+        iface.types.retain(|_, ty| self.types[ty.index()].is_some());
+
         // NB: note that `iface.doc` is not updated here since interfaces
         // haven't been mapped yet and that's done in a separate step.
         for (_name, ty) in iface.types.iter_mut() {
-            self.update_type_id(ty);
+            self.update_type_id(ty, spans.map(|s| s.span))?;
         }
         if let Some(spans) = spans {
             assert_eq!(iface.functions.len(), spans.funcs.len());
         }
         for (i, (_, func)) in iface.functions.iter_mut().enumerate() {
+            if !resolve.include_stability(&func.stability) {
+                continue;
+            }
             let span = spans.map(|s| s.funcs[i]);
-            self.update_function(resolve, func, span)?;
+            self.update_function(resolve, func, span)
+                .with_context(|| format!("failed to update function `{}`", func.name))?;
         }
+        iface
+            .functions
+            .retain(|_, f| resolve.include_stability(&f.stability));
         Ok(())
     }
 
@@ -1509,19 +1634,19 @@ impl Remap {
         match &mut func.kind {
             FunctionKind::Freestanding => {}
             FunctionKind::Method(id) | FunctionKind::Constructor(id) | FunctionKind::Static(id) => {
-                self.update_type_id(id);
+                self.update_type_id(id, span)?;
             }
         }
         for (_, ty) in func.params.iter_mut() {
-            self.update_ty(resolve, ty);
+            self.update_ty(resolve, ty, span)?;
         }
         match &mut func.results {
             Results::Named(named) => {
                 for (_, ty) in named.iter_mut() {
-                    self.update_ty(resolve, ty);
+                    self.update_ty(resolve, ty, span)?;
                 }
             }
-            Results::Anon(ty) => self.update_ty(resolve, ty),
+            Results::Anon(ty) => self.update_ty(resolve, ty, span)?,
         }
 
         for ty in func.results.iter_types() {
@@ -1530,13 +1655,13 @@ impl Remap {
             }
             match span {
                 Some(span) => {
-                    bail!(Error {
+                    bail!(Error::new(
                         span,
-                        msg: format!(
+                        format!(
                             "function returns a type which contains \
                              a `borrow<T>` which is not supported"
                         )
-                    })
+                    ))
                 }
                 None => unreachable!(),
             }
@@ -1569,22 +1694,30 @@ impl Remap {
         // determining names later on.
         let mut import_funcs = Vec::new();
         let mut import_types = Vec::new();
-        for ((mut name, item), span) in mem::take(&mut world.imports)
+        for ((mut name, mut item), span) in mem::take(&mut world.imports)
             .into_iter()
             .zip(&spans.imports)
         {
-            self.update_world_key(&mut name);
+            // Update the `id` eagerly here so `item.stability(..)` below
+            // works.
+            if let WorldItem::Type(id) = &mut item {
+                *id = self.map_type(*id, Some(*span))?;
+            }
+            let stability = item.stability(resolve);
+            if !resolve.include_stability(stability) {
+                continue;
+            }
+            self.update_world_key(&mut name, Some(*span))?;
             match item {
-                WorldItem::Interface(id) => {
-                    let id = self.interfaces[id.index()];
-                    self.add_world_import(resolve, world, name, id);
+                WorldItem::Interface { id, stability } => {
+                    let id = self.map_interface(id, Some(*span))?;
+                    self.add_world_import(resolve, world, name, id, &stability);
                 }
                 WorldItem::Function(mut f) => {
                     self.update_function(resolve, &mut f, Some(*span))?;
                     import_funcs.push((name.unwrap_name(), f, *span));
                 }
                 WorldItem::Type(id) => {
-                    let id = self.types[id.index()];
                     import_types.push((name.unwrap_name(), id, *span));
                 }
             }
@@ -1594,7 +1727,13 @@ impl Remap {
             if let TypeDefKind::Type(Type::Id(other)) = resolve.types[*id].kind {
                 if let TypeOwner::Interface(owner) = resolve.types[other].owner {
                     let name = WorldKey::Interface(owner);
-                    self.add_world_import(resolve, world, name, owner);
+                    self.add_world_import(
+                        resolve,
+                        world,
+                        name,
+                        owner,
+                        &resolve.types[*id].stability,
+                    );
                 }
             }
         }
@@ -1605,11 +1744,15 @@ impl Remap {
             .into_iter()
             .zip(&spans.exports)
         {
-            self.update_world_key(&mut name);
+            let stability = item.stability(resolve);
+            if !resolve.include_stability(stability) {
+                continue;
+            }
+            self.update_world_key(&mut name, Some(*span))?;
             match item {
-                WorldItem::Interface(id) => {
-                    let id = self.interfaces[id.index()];
-                    let prev = export_interfaces.insert(id, (name, *span));
+                WorldItem::Interface { id, stability } => {
+                    let id = self.map_interface(id, Some(*span))?;
+                    let prev = export_interfaces.insert(id, (name, *span, stability));
                     assert!(prev.is_none());
                 }
                 WorldItem::Function(mut f) => {
@@ -1630,9 +1773,14 @@ impl Remap {
         assert_eq!(world.includes.len(), spans.includes.len());
         let includes = mem::take(&mut world.includes);
         let include_names = mem::take(&mut world.include_names);
-        for (index, (include_world, span)) in includes.into_iter().zip(&spans.includes).enumerate()
+        for (((stability, include_world), span), names) in includes
+            .into_iter()
+            .zip(&spans.includes)
+            .zip(&include_names)
         {
-            let names = &include_names[index];
+            if !resolve.include_stability(&stability) {
+                continue;
+            }
             self.resolve_include(world, include_world, names, *span, resolve)?;
         }
 
@@ -1641,10 +1789,10 @@ impl Remap {
                 .imports
                 .insert(WorldKey::Name(name.clone()), WorldItem::Type(id));
             if prev.is_some() {
-                bail!(Error {
-                    msg: format!("export of type `{name}` shadows previously imported interface"),
+                bail!(Error::new(
                     span,
-                })
+                    format!("export of type `{name}` shadows previously imported interface"),
+                ))
             }
         }
 
@@ -1653,12 +1801,10 @@ impl Remap {
                 .imports
                 .insert(WorldKey::Name(name.clone()), WorldItem::Function(func));
             if prev.is_some() {
-                bail!(Error {
-                    msg: format!(
-                        "import of function `{name}` shadows previously imported interface"
-                    ),
+                bail!(Error::new(
                     span,
-                })
+                    format!("import of function `{name}` shadows previously imported interface"),
+                ))
             }
         }
 
@@ -1667,12 +1813,10 @@ impl Remap {
                 .exports
                 .insert(WorldKey::Name(name.clone()), WorldItem::Function(func));
             if prev.is_some() {
-                bail!(Error {
-                    msg: format!(
-                        "export of function `{name}` shadows previously exported interface"
-                    ),
+                bail!(Error::new(
                     span,
-                })
+                    format!("export of function `{name}` shadows previously exported interface"),
+                ))
             }
         }
 
@@ -1697,7 +1841,7 @@ impl Remap {
             let rank = |item: &WorldItem| match item {
                 WorldItem::Type(_) => unreachable!(),
                 WorldItem::Function(_) => 0,
-                WorldItem::Interface(_) => 1,
+                WorldItem::Interface { .. } => 1,
             };
             rank(a).cmp(&rank(b))
         });
@@ -1708,13 +1852,14 @@ impl Remap {
         Ok(())
     }
 
-    fn update_world_key(&self, key: &mut WorldKey) {
+    fn update_world_key(&self, key: &mut WorldKey, span: Option<Span>) -> Result<()> {
         match key {
             WorldKey::Name(_) => {}
             WorldKey::Interface(id) => {
-                *id = self.interfaces[id.index()];
+                *id = self.map_interface(*id, span)?;
             }
         }
+        Ok(())
     }
 
     fn add_world_import(
@@ -1723,14 +1868,21 @@ impl Remap {
         world: &mut World,
         key: WorldKey,
         id: InterfaceId,
+        stability: &Stability,
     ) {
         if world.imports.contains_key(&key) {
             return;
         }
         for dep in resolve.interface_direct_deps(id) {
-            self.add_world_import(resolve, world, WorldKey::Interface(dep), dep);
+            self.add_world_import(resolve, world, WorldKey::Interface(dep), dep, stability);
         }
-        let prev = world.imports.insert(key, WorldItem::Interface(id));
+        let prev = world.imports.insert(
+            key,
+            WorldItem::Interface {
+                id,
+                stability: stability.clone(),
+            },
+        );
         assert!(prev.is_none());
     }
 
@@ -1784,10 +1936,10 @@ impl Remap {
         &self,
         resolve: &Resolve,
         world: &mut World,
-        export_interfaces: &IndexMap<InterfaceId, (WorldKey, Span)>,
+        export_interfaces: &IndexMap<InterfaceId, (WorldKey, Span, Stability)>,
     ) -> Result<()> {
         let mut required_imports = HashSet::new();
-        for (id, (key, span)) in export_interfaces.iter() {
+        for (id, (key, span, stability)) in export_interfaces.iter() {
             let ok = add_world_export(
                 resolve,
                 world,
@@ -1796,9 +1948,11 @@ impl Remap {
                 *id,
                 key,
                 true,
+                &stability,
             );
             if !ok {
-                bail!(Error {
+                bail!(Error::new(
+                    *span,
                     // FIXME: this is not a great error message and basically no
                     // one will know what to do when it gets printed. Improving
                     // this error message, however, is a chunk of work that may
@@ -1816,12 +1970,11 @@ impl Remap {
                     // more refactoring, so it's left to a future date in the
                     // hopes that most folks won't actually run into this for
                     // the time being.
-                    msg: format!(
+                    format!(
                         "interface transitively depends on an interface in \
                          incompatible ways",
                     ),
-                    span: *span,
-                });
+                ));
             }
         }
         return Ok(());
@@ -1829,11 +1982,12 @@ impl Remap {
         fn add_world_export(
             resolve: &Resolve,
             world: &mut World,
-            export_interfaces: &IndexMap<InterfaceId, (WorldKey, Span)>,
+            export_interfaces: &IndexMap<InterfaceId, (WorldKey, Span, Stability)>,
             required_imports: &mut HashSet<InterfaceId>,
             id: InterfaceId,
             key: &WorldKey,
             add_export: bool,
+            stability: &Stability,
         ) -> bool {
             if world.exports.contains_key(key) {
                 if add_export {
@@ -1858,19 +2012,24 @@ impl Remap {
                     dep,
                     &key,
                     add_export,
+                    stability,
                 )
             });
             if !ok {
                 return false;
             }
+            let item = WorldItem::Interface {
+                id,
+                stability: stability.clone(),
+            };
             if add_export {
                 if required_imports.contains(&id) {
                     return false;
                 }
-                world.exports.insert(key.clone(), WorldItem::Interface(id));
+                world.exports.insert(key.clone(), item);
             } else {
                 required_imports.insert(id);
-                world.imports.insert(key.clone(), WorldItem::Interface(id));
+                world.imports.insert(key.clone(), item);
             }
             true
         }
@@ -1884,7 +2043,7 @@ impl Remap {
         span: Span,
         resolve: &Resolve,
     ) -> Result<()> {
-        let include_world_id = self.worlds[include_world.index()];
+        let include_world_id = self.map_world(include_world, Some(span))?;
         let include_world = &resolve.worlds[include_world_id];
         let mut names_ = names.to_owned();
 
@@ -1896,10 +2055,10 @@ impl Remap {
             self.remove_matching_name(export, &mut names_);
         }
         if !names_.is_empty() {
-            bail!(Error {
-                msg: format!("no import or export kebab-name `{}`. Note that an ID does not support renaming", names_[0].name),
-                span: span,
-            });
+            bail!(Error::new(
+                span,
+                format!("no import or export kebab-name `{}`. Note that an ID does not support renaming", names_[0].name),
+            ));
         }
 
         // copy the imports and exports from the included world into the current world
@@ -1934,10 +2093,10 @@ impl Remap {
 
                 let prev = items.insert(WorldKey::Name(n.clone()), item.1.clone());
                 if prev.is_some() {
-                    bail!(Error {
-                        msg: format!("{item_type} of `{n}` shadows previously {item_type}ed items"),
+                    bail!(Error::new(
                         span,
-                    })
+                        format!("{item_type} of `{n}` shadows previously {item_type}ed items"),
+                    ))
                 }
             }
             key => {
@@ -2232,7 +2391,7 @@ impl<'a> MergeMap<'a> {
 
     fn match_world_item(&mut self, from: &WorldItem, into: &WorldItem) -> Result<()> {
         match (from, into) {
-            (WorldItem::Interface(from), WorldItem::Interface(into)) => {
+            (WorldItem::Interface { id: from, .. }, WorldItem::Interface { id: into, .. }) => {
                 match (
                     &self.from.interfaces[*from].name,
                     &self.into.interfaces[*into].name,
@@ -2265,7 +2424,7 @@ impl<'a> MergeMap<'a> {
                 assert!(prev.is_none());
             }
 
-            (WorldItem::Interface(_), _)
+            (WorldItem::Interface { .. }, _)
             | (WorldItem::Function(_), _)
             | (WorldItem::Type(_), _) => {
                 bail!("world items do not have the same type")
diff --git a/crates/wit-parser/src/serde_.rs b/crates/wit-parser/src/serde_.rs
index b554d2e6..e78e1f75 100644
--- a/crates/wit-parser/src/serde_.rs
+++ b/crates/wit-parser/src/serde_.rs
@@ -1,8 +1,9 @@
 use crate::{Params, Type};
 use id_arena::{Arena, Id};
 use indexmap::IndexMap;
+use semver::Version;
 use serde::ser::{SerializeMap, SerializeSeq, Serializer};
-use serde::Serialize;
+use serde::{de::Error, Deserialize, Serialize};
 
 pub fn serialize_none<S>(serializer: S) -> Result<S::Ok, S::Error>
 where
@@ -106,3 +107,18 @@ struct Param {
     #[serde(rename = "type")]
     pub typ: Type,
 }
+
+pub fn serialize_version<S>(version: &Version, serializer: S) -> Result<S::Ok, S::Error>
+where
+    S: Serializer,
+{
+    version.to_string().serialize(serializer)
+}
+
+pub fn deserialize_version<'de, D>(deserializer: D) -> Result<Version, D::Error>
+where
+    D: serde::de::Deserializer<'de>,
+{
+    let version: String = String::deserialize(deserializer)?;
+    version.parse().map_err(|e| D::Error::custom(e))
+}
diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs
index bc3615dd..366d325f 100644
--- a/src/bin/wasm-tools/component.rs
+++ b/src/bin/wasm-tools/component.rs
@@ -13,7 +13,7 @@ use wat::Detect;
 use wit_component::{
     embed_component_metadata, ComponentEncoder, DecodedWasm, Linker, StringEncoding, WitPrinter,
 };
-use wit_parser::{Resolve, UnresolvedPackage};
+use wit_parser::{PackageId, Resolve, UnresolvedPackage};
 
 /// WebAssembly wit-based component tooling.
 #[derive(Parser)]
@@ -170,6 +170,42 @@ impl NewOpts {
     }
 }
 
+#[derive(Parser)]
+struct WitResolve {
+    /// Path to WIT files to load.
+    ///
+    /// This can be a directory containing `*.wit` files, a `*.wit` file itself,
+    /// or a `*.wasm` file which is a WIT package encoded as WebAssembly.
+    wit: PathBuf,
+
+    /// Features to enable when parsing the `wit` option.
+    ///
+    /// This flag enables the `@unstable` feature in WIT documents where the
+    /// items are otherwise hidden by default.
+    #[clap(long)]
+    features: Vec<String>,
+}
+
+impl WitResolve {
+    fn resolve_with_features(features: &[String]) -> Resolve {
+        let mut resolve = Resolve::default();
+        for feature in features {
+            for f in feature.split_whitespace() {
+                for f in f.split(',').filter(|s| !s.is_empty()) {
+                    resolve.features.insert(f.to_string());
+                }
+            }
+        }
+        return resolve;
+    }
+
+    fn load(&self) -> Result<(Resolve, PackageId)> {
+        let mut resolve = Self::resolve_with_features(&self.features);
+        let id = resolve.push_path(&self.wit)?.0;
+        Ok((resolve, id))
+    }
+}
+
 /// Embeds metadata for a component inside of a core wasm module.
 ///
 /// This subcommand is a convenience tool provided for producing core wasm
@@ -187,11 +223,8 @@ impl NewOpts {
 /// working with text format wasm.
 #[derive(Parser)]
 pub struct EmbedOpts {
-    /// The WIT package where the `world` that the core wasm module implements
-    /// lives.
-    ///
-    /// This can either be a directory or a path to a single `*.wit` file.
-    wit: PathBuf,
+    #[clap(flatten)]
+    resolve: WitResolve,
 
     #[clap(flatten)]
     io: wasm_tools::InputOutput,
@@ -242,8 +275,7 @@ impl EmbedOpts {
         } else {
             Some(self.io.parse_input_wasm()?)
         };
-        let mut resolve = Resolve::default();
-        let id = resolve.push_path(&self.wit)?.0;
+        let (resolve, id) = self.resolve.load()?;
         let world = resolve.select_world(id, self.world.as_deref())?;
 
         let mut wasm = wasm.unwrap_or_else(|| wit_component::dummy_module(&resolve, world));
@@ -461,6 +493,13 @@ pub struct WitOpts {
         conflicts_with = "wat"
     )]
     json: bool,
+
+    /// Features to enable when parsing the `wit` option.
+    ///
+    /// This flag enables the `@unstable` feature in WIT documents where the
+    /// items are otherwise hidden by default.
+    #[clap(long)]
+    features: Vec<String>,
 }
 
 impl WitOpts {
@@ -491,7 +530,7 @@ impl WitOpts {
         // `parse_wit_from_path`.
         if let Some(input) = &self.input {
             if input.is_dir() {
-                let mut resolve = Resolve::default();
+                let mut resolve = WitResolve::resolve_with_features(&self.features);
                 let id = resolve.push_dir(&input)?.0;
                 return Ok(DecodedWasm::WitPackage(resolve, id));
             }
@@ -539,7 +578,7 @@ impl WitOpts {
                     Ok(s) => s,
                     Err(_) => bail!("input was not valid utf-8"),
                 };
-                let mut resolve = Resolve::default();
+                let mut resolve = WitResolve::resolve_with_features(&self.features);
                 let pkg = UnresolvedPackage::parse(path, input)?;
                 let id = resolve.push(pkg)?;
                 Ok(DecodedWasm::WitPackage(resolve, id))
@@ -645,10 +684,8 @@ pub struct TargetsOpts {
     #[clap(flatten)]
     general: wasm_tools::GeneralOpts,
 
-    /// The WIT package containing the `world` used to test a component for conformance.
-    ///
-    /// This can either be a directory or a path to a single `*.wit` file.
-    wit: PathBuf,
+    #[clap(flatten)]
+    resolve: WitResolve,
 
     /// The world used to test whether a component conforms to its signature.
     #[clap(short, long)]
@@ -665,8 +702,7 @@ impl TargetsOpts {
 
     /// Executes the application.
     fn run(self) -> Result<()> {
-        let mut resolve = Resolve::default();
-        let package_id = resolve.push_path(&self.wit)?.0;
+        let (resolve, package_id) = self.resolve.load()?;
         let world = resolve.select_world(package_id, self.world.as_deref())?;
         let component_to_test = self.input.parse_wasm()?;
 
@@ -683,12 +719,8 @@ pub struct SemverCheckOpts {
     #[clap(flatten)]
     general: wasm_tools::GeneralOpts,
 
-    /// The WIT package containing the `prev` and `new` worlds used in
-    /// arguments.
-    ///
-    /// This can either be a directory, a path to a single `*.wit` file, or a
-    /// path to a wasm-encoded WIT package.
-    wit: PathBuf,
+    #[clap(flatten)]
+    resolve: WitResolve,
 
     /// The "previous" world, or older version, of what's being tested.
     ///
@@ -710,8 +742,7 @@ impl SemverCheckOpts {
     }
 
     fn run(self) -> Result<()> {
-        let mut resolve = Resolve::default();
-        let package_id = resolve.push_path(&self.wit)?.0;
+        let (resolve, package_id) = self.resolve.load()?;
         let prev = resolve.select_world(package_id, Some(&self.prev))?;
         let new = resolve.select_world(package_id, Some(&self.new))?;
         wit_component::semver_check(resolve, prev, new)?;


Test patch:
diff --git a/crates/wit-parser/tests/all.rs b/crates/wit-parser/tests/all.rs
index bdb66321..6c7af1f7 100644
--- a/crates/wit-parser/tests/all.rs
+++ b/crates/wit-parser/tests/all.rs
@@ -75,6 +75,7 @@ struct Runner {}
 impl Runner {
     fn run(&mut self, test: &Path) -> Result<()> {
         let mut resolve = Resolve::new();
+        resolve.features.insert("active".to_string());
         let result = resolve.push_path(test);
         let result = if test.iter().any(|s| s == "parse-fail") {
             match result {
diff --git a/crates/wit-parser/tests/ui/complex-include.wit.json b/crates/wit-parser/tests/ui/complex-include.wit.json
index 2c657bea..f9b17f27 100644
--- a/crates/wit-parser/tests/ui/complex-include.wit.json
+++ b/crates/wit-parser/tests/ui/complex-include.wit.json
@@ -4,10 +4,14 @@
       "name": "bar-a",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         }
       },
       "exports": {},
@@ -17,10 +21,14 @@
       "name": "baz-a",
       "imports": {
         "interface-2": {
-          "interface": 2
+          "interface": {
+            "id": 2
+          }
         },
         "interface-3": {
-          "interface": 3
+          "interface": {
+            "id": 3
+          }
         }
       },
       "exports": {},
@@ -30,10 +38,14 @@
       "name": "a",
       "imports": {
         "interface-4": {
-          "interface": 4
+          "interface": {
+            "id": 4
+          }
         },
         "interface-5": {
-          "interface": 5
+          "interface": {
+            "id": 5
+          }
         }
       },
       "exports": {},
@@ -43,10 +55,14 @@
       "name": "b",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         }
       },
       "exports": {},
@@ -56,10 +72,14 @@
       "name": "c",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         }
       },
       "exports": {},
@@ -69,22 +89,34 @@
       "name": "union-world",
       "imports": {
         "interface-4": {
-          "interface": 4
+          "interface": {
+            "id": 4
+          }
         },
         "interface-5": {
-          "interface": 5
+          "interface": {
+            "id": 5
+          }
         },
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         },
         "interface-2": {
-          "interface": 2
+          "interface": {
+            "id": 2
+          }
         },
         "interface-3": {
-          "interface": 3
+          "interface": {
+            "id": 3
+          }
         }
       },
       "exports": {},
diff --git a/crates/wit-parser/tests/ui/diamond1.wit.json b/crates/wit-parser/tests/ui/diamond1.wit.json
index 8d7ce082..54e87e7b 100644
--- a/crates/wit-parser/tests/ui/diamond1.wit.json
+++ b/crates/wit-parser/tests/ui/diamond1.wit.json
@@ -4,10 +4,14 @@
       "name": "foo",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         }
       },
       "exports": {},
diff --git a/crates/wit-parser/tests/ui/disambiguate-diamond.wit.json b/crates/wit-parser/tests/ui/disambiguate-diamond.wit.json
index 0e3ebea3..34f675da 100644
--- a/crates/wit-parser/tests/ui/disambiguate-diamond.wit.json
+++ b/crates/wit-parser/tests/ui/disambiguate-diamond.wit.json
@@ -4,16 +4,24 @@
       "name": "foo",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "foo": {
-          "interface": 2
+          "interface": {
+            "id": 2
+          }
         },
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         },
         "bar": {
-          "interface": 3
+          "interface": {
+            "id": 3
+          }
         }
       },
       "exports": {},
diff --git a/crates/wit-parser/tests/ui/feature-gates.wit b/crates/wit-parser/tests/ui/feature-gates.wit
new file mode 100644
index 00000000..019471d3
--- /dev/null
+++ b/crates/wit-parser/tests/ui/feature-gates.wit
@@ -0,0 +1,118 @@
+package a:b;
+
+@unstable(feature = not-active)
+interface gated {
+}
+
+@unstable(feature = active)
+interface ungated {
+  @unstable(feature = not-active)
+  gated: func();
+
+  @unstable(feature = active)
+  ungated: func();
+}
+
+@unstable(feature = active)
+interface ungated2 {
+  @unstable(feature = not-active)
+  type gated = u32;
+  @unstable(feature = not-active)
+  type gated2 = gated;
+
+  @unstable(feature = not-active)
+  type gated-with-anonymous-type = option<option<gated>>;
+
+  @unstable(feature = active)
+  type ungated = u32;
+  @unstable(feature = active)
+  type ungated2 = ungated;
+}
+
+@unstable(feature = inactive)
+interface gated-use-target {
+  @unstable(feature = inactive)
+  type t = u32;
+}
+
+@unstable(feature = inactive)
+interface gated-use {
+  @unstable(feature = inactive)
+  use gated-use-target.{t};
+}
+
+@unstable(feature = active)
+interface ungated-use-target {
+  @unstable(feature = active)
+  type t = u32;
+}
+
+@unstable(feature = active)
+interface ungated-use {
+  @unstable(feature = active)
+  use ungated-use-target.{t};
+}
+
+@unstable(feature = inactive)
+interface gated-for-world {}
+
+@unstable(feature = inactive)
+world gated-world {
+  @unstable(feature = inactive)
+  import gated-for-world;
+  @unstable(feature = inactive)
+  export gated-for-world;
+}
+
+@unstable(feature = active)
+interface ungated-for-world {}
+
+@unstable(feature = active)
+world ungated-world {
+  @unstable(feature = active)
+  import ungated-for-world;
+  @unstable(feature = active)
+  export ungated-for-world;
+}
+
+world mixed-world {
+  @unstable(feature = inactive)
+  import gated-for-world;
+  @unstable(feature = inactive)
+  export gated-for-world;
+  @unstable(feature = inactive)
+  include gated-world;
+
+  @unstable(feature = active)
+  import ungated-for-world;
+  @unstable(feature = active)
+  export ungated-for-world;
+  @unstable(feature = inactive)
+  include ungated-world;
+}
+
+interface with-resources {
+  @unstable(feature = inactive)
+  resource gated {
+    @unstable(feature = inactive)
+    constructor();
+
+    @unstable(feature = inactive)
+    x: static func();
+
+    @unstable(feature = inactive)
+    y: func();
+  }
+
+  @unstable(feature = active)
+  resource ungated {
+    @unstable(feature = active)
+    constructor();
+
+    @unstable(feature = active)
+    x: static func();
+
+    @unstable(feature = active)
+    y: func();
+  }
+}
diff --git a/crates/wit-parser/tests/ui/feature-gates.wit.json b/crates/wit-parser/tests/ui/feature-gates.wit.json
new file mode 100644
index 00000000..e211f045
--- /dev/null
+++ b/crates/wit-parser/tests/ui/feature-gates.wit.json
@@ -0,0 +1,288 @@
+{
+  "worlds": [
+    {
+      "name": "ungated-world",
+      "imports": {
+        "interface-4": {
+          "interface": {
+            "id": 4,
+            "stability": {
+              "type": "unstable",
+              "feature": "active"
+            }
+          }
+        }
+      },
+      "exports": {
+        "interface-4": {
+          "interface": {
+            "id": 4,
+            "stability": {
+              "type": "unstable",
+              "feature": "active"
+            }
+          }
+        }
+      },
+      "package": 0,
+      "stability": {
+        "type": "unstable",
+        "feature": "active"
+      }
+    },
+    {
+      "name": "mixed-world",
+      "imports": {
+        "interface-4": {
+          "interface": {
+            "id": 4,
+            "stability": {
+              "type": "unstable",
+              "feature": "active"
+            }
+          }
+        }
+      },
+      "exports": {
+        "interface-4": {
+          "interface": {
+            "id": 4,
+            "stability": {
+              "type": "unstable",
+              "feature": "active"
+            }
+          }
+        }
+      },
+      "package": 0
+    }
+  ],
+  "interfaces": [
+    {
+      "name": "ungated",
+      "types": {},
+      "functions": {
+        "ungated": {
+          "name": "ungated",
+          "kind": "freestanding",
+          "params": [],
+          "results": [],
+          "stability": {
+            "type": "unstable",
+            "feature": "active"
+          }
+        }
+      },
+      "stability": {
+        "type": "unstable",
+        "feature": "active"
+      },
+      "package": 0
+    },
+    {
+      "name": "ungated2",
+      "types": {
+        "ungated": 0,
+        "ungated2": 1
+      },
+      "functions": {},
+      "stability": {
+        "type": "unstable",
+        "feature": "active"
+      },
+      "package": 0
+    },
+    {
+      "name": "ungated-use-target",
+      "types": {
+        "t": 2
+      },
+      "functions": {},
+      "stability": {
+        "type": "unstable",
+        "feature": "active"
+      },
+      "package": 0
+    },
+    {
+      "name": "ungated-use",
+      "types": {
+        "t": 3
+      },
+      "functions": {},
+      "stability": {
+        "type": "unstable",
+        "feature": "active"
+      },
+      "package": 0
+    },
+    {
+      "name": "ungated-for-world",
+      "types": {},
+      "functions": {},
+      "stability": {
+        "type": "unstable",
+        "feature": "active"
+      },
+      "package": 0
+    },
+    {
+      "name": "with-resources",
+      "types": {
+        "ungated": 4
+      },
+      "functions": {
+        "[constructor]ungated": {
+          "name": "[constructor]ungated",
+          "kind": {
+            "constructor": 4
+          },
+          "params": [],
+          "results": [
+            {
+              "type": 6
+            }
+          ],
+          "stability": {
+            "type": "unstable",
+            "feature": "active"
+          }
+        },
+        "[static]ungated.x": {
+          "name": "[static]ungated.x",
+          "kind": {
+            "static": 4
+          },
+          "params": [],
+          "results": [],
+          "stability": {
+            "type": "unstable",
+            "feature": "active"
+          }
+        },
+        "[method]ungated.y": {
+          "name": "[method]ungated.y",
+          "kind": {
+            "method": 4
+          },
+          "params": [
+            {
+              "name": "self",
+              "type": 5
+            }
+          ],
+          "results": [],
+          "stability": {
+            "type": "unstable",
+            "feature": "active"
+          }
+        }
+      },
+      "package": 0
+    }
+  ],
+  "types": [
+    {
+      "name": "ungated",
+      "kind": {
+        "type": "u32"
+      },
+      "owner": {
+        "interface": 1
+      },
+      "stability": {
+        "type": "unstable",
+        "feature": "active"
+      }
+    },
+    {
+      "name": "ungated2",
+      "kind": {
+        "type": 0
+      },
+      "owner": {
+        "interface": 1
+      },
+      "stability": {
+        "type": "unstable",
+        "feature": "active"
+      }
+    },
+    {
+      "name": "t",
+      "kind": {
+        "type": "u32"
+      },
+      "owner": {
+        "interface": 2
+      },
+      "stability": {
+        "type": "unstable",
+        "feature": "active"
+      }
+    },
+    {
+      "name": "t",
+      "kind": {
+        "type": 2
+      },
+      "owner": {
+        "interface": 3
+      },
+      "stability": {
+        "type": "unstable",
+        "feature": "active"
+      }
+    },
+    {
+      "name": "ungated",
+      "kind": "resource",
+      "owner": {
+        "interface": 5
+      },
+      "stability": {
+        "type": "unstable",
+        "feature": "active"
+      }
+    },
+    {
+      "name": null,
+      "kind": {
+        "handle": {
+          "borrow": 4
+        }
+      },
+      "owner": null,
+      "stability": {
+        "type": "unstable",
+        "feature": "active"
+      }
+    },
+    {
+      "name": null,
+      "kind": {
+        "handle": {
+          "own": 4
+        }
+      },
+      "owner": null
+    }
+  ],
+  "packages": [
+    {
+      "name": "a:b",
+      "interfaces": {
+        "ungated": 0,
+        "ungated2": 1,
+        "ungated-use-target": 2,
+        "ungated-use": 3,
+        "ungated-for-world": 4,
+        "with-resources": 5
+      },
+      "worlds": {
+        "ungated-world": 0,
+        "mixed-world": 1
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/foreign-deps-union.wit.json b/crates/wit-parser/tests/ui/foreign-deps-union.wit.json
index 3c1e2fb4..419d62e7 100644
--- a/crates/wit-parser/tests/ui/foreign-deps-union.wit.json
+++ b/crates/wit-parser/tests/ui/foreign-deps-union.wit.json
@@ -4,10 +4,14 @@
       "name": "wasi",
       "imports": {
         "interface-8": {
-          "interface": 8
+          "interface": {
+            "id": 8
+          }
         },
         "interface-7": {
-          "interface": 7
+          "interface": {
+            "id": 7
+          }
         }
       },
       "exports": {},
@@ -17,15 +21,21 @@
       "name": "my-world",
       "imports": {
         "interface-8": {
-          "interface": 8
+          "interface": {
+            "id": 8
+          }
         },
         "interface-7": {
-          "interface": 7
+          "interface": {
+            "id": 7
+          }
         }
       },
       "exports": {
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         }
       },
       "package": 6
@@ -34,18 +44,26 @@
       "name": "my-world2",
       "imports": {
         "interface-8": {
-          "interface": 8
+          "interface": {
+            "id": 8
+          }
         },
         "interface-7": {
-          "interface": 7
+          "interface": {
+            "id": 7
+          }
         }
       },
       "exports": {
         "interface-9": {
-          "interface": 9
+          "interface": {
+            "id": 9
+          }
         },
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         }
       },
       "package": 6
@@ -54,10 +72,14 @@
       "name": "bars-world",
       "imports": {
         "interface-4": {
-          "interface": 4
+          "interface": {
+            "id": 4
+          }
         },
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         }
       },
       "exports": {},
@@ -67,18 +89,26 @@
       "name": "unionw-world",
       "imports": {
         "interface-8": {
-          "interface": 8
+          "interface": {
+            "id": 8
+          }
         },
         "interface-7": {
-          "interface": 7
+          "interface": {
+            "id": 7
+          }
         }
       },
       "exports": {
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         },
         "interface-9": {
-          "interface": 9
+          "interface": {
+            "id": 9
+          }
         }
       },
       "package": 6
diff --git a/crates/wit-parser/tests/ui/foreign-deps.wit.json b/crates/wit-parser/tests/ui/foreign-deps.wit.json
index da5020f0..503c1b32 100644
--- a/crates/wit-parser/tests/ui/foreign-deps.wit.json
+++ b/crates/wit-parser/tests/ui/foreign-deps.wit.json
@@ -4,15 +4,21 @@
       "name": "my-world",
       "imports": {
         "interface-8": {
-          "interface": 8
+          "interface": {
+            "id": 8
+          }
         },
         "interface-7": {
-          "interface": 7
+          "interface": {
+            "id": 7
+          }
         }
       },
       "exports": {
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         }
       },
       "package": 6
@@ -21,18 +27,26 @@
       "name": "my-world2",
       "imports": {
         "interface-8": {
-          "interface": 8
+          "interface": {
+            "id": 8
+          }
         },
         "interface-7": {
-          "interface": 7
+          "interface": {
+            "id": 7
+          }
         }
       },
       "exports": {
         "interface-9": {
-          "interface": 9
+          "interface": {
+            "id": 9
+          }
         },
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         }
       },
       "package": 6
@@ -41,10 +55,14 @@
       "name": "bars-world",
       "imports": {
         "interface-4": {
-          "interface": 4
+          "interface": {
+            "id": 4
+          }
         },
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         }
       },
       "exports": {},
diff --git a/crates/wit-parser/tests/ui/ignore-files-deps.wit.json b/crates/wit-parser/tests/ui/ignore-files-deps.wit.json
index 1b9d00c3..7a812dbe 100644
--- a/crates/wit-parser/tests/ui/ignore-files-deps.wit.json
+++ b/crates/wit-parser/tests/ui/ignore-files-deps.wit.json
@@ -4,7 +4,9 @@
       "name": "foo",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         }
       },
       "exports": {},
diff --git a/crates/wit-parser/tests/ui/import-export-overlap2.wit.json b/crates/wit-parser/tests/ui/import-export-overlap2.wit.json
index fff5bfd3..0c991269 100644
--- a/crates/wit-parser/tests/ui/import-export-overlap2.wit.json
+++ b/crates/wit-parser/tests/ui/import-export-overlap2.wit.json
@@ -14,7 +14,9 @@
       },
       "exports": {
         "a": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         }
       },
       "package": 0
diff --git a/crates/wit-parser/tests/ui/include-reps.wit.json b/crates/wit-parser/tests/ui/include-reps.wit.json
index 8217e2ee..21debef2 100644
--- a/crates/wit-parser/tests/ui/include-reps.wit.json
+++ b/crates/wit-parser/tests/ui/include-reps.wit.json
@@ -4,12 +4,16 @@
       "name": "bar",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         }
       },
       "exports": {
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         }
       },
       "package": 0
@@ -18,12 +22,16 @@
       "name": "foo",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         }
       },
       "exports": {
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         }
       },
       "package": 0
diff --git a/crates/wit-parser/tests/ui/kinds-of-deps.wit.json b/crates/wit-parser/tests/ui/kinds-of-deps.wit.json
index 07f01a7c..f13afe63 100644
--- a/crates/wit-parser/tests/ui/kinds-of-deps.wit.json
+++ b/crates/wit-parser/tests/ui/kinds-of-deps.wit.json
@@ -4,16 +4,24 @@
       "name": "a",
       "imports": {
         "interface-2": {
-          "interface": 2
+          "interface": {
+            "id": 2
+          }
         },
         "interface-3": {
-          "interface": 3
+          "interface": {
+            "id": 3
+          }
         },
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         }
       },
       "exports": {},
diff --git a/crates/wit-parser/tests/ui/many-names.wit.json b/crates/wit-parser/tests/ui/many-names.wit.json
index 87d111bd..590c6847 100644
--- a/crates/wit-parser/tests/ui/many-names.wit.json
+++ b/crates/wit-parser/tests/ui/many-names.wit.json
@@ -4,7 +4,9 @@
       "name": "name",
       "imports": {
         "name": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         }
       },
       "exports": {},
diff --git a/crates/wit-parser/tests/ui/multi-file.wit.json b/crates/wit-parser/tests/ui/multi-file.wit.json
index ef6171bc..45af0f5d 100644
--- a/crates/wit-parser/tests/ui/multi-file.wit.json
+++ b/crates/wit-parser/tests/ui/multi-file.wit.json
@@ -4,12 +4,16 @@
       "name": "more-depends-on-later-things",
       "imports": {
         "interface-3": {
-          "interface": 3
+          "interface": {
+            "id": 3
+          }
         }
       },
       "exports": {
         "interface-3": {
-          "interface": 3
+          "interface": {
+            "id": 3
+          }
         }
       },
       "package": 0
@@ -18,7 +22,9 @@
       "name": "the-world",
       "imports": {
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         },
         "x": {
           "type": 15
diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-gate1.wit b/crates/wit-parser/tests/ui/parse-fail/bad-gate1.wit
new file mode 100644
index 00000000..36347f5c
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/bad-gate1.wit
@@ -0,0 +1,8 @@
+package a:b;
+
+interface foo {
+  @unstable(feature = not-active)
+  type gated = u32;
+
+  type foo = gated;
+}
diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-gate1.wit.result b/crates/wit-parser/tests/ui/parse-fail/bad-gate1.wit.result
new file mode 100644
index 00000000..3fb29c08
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/bad-gate1.wit.result
@@ -0,0 +1,5 @@
+found a reference to a type which is excluded due to its feature not being activated
+     --> tests/ui/parse-fail/bad-gate1.wit:7:8
+      |
+    7 |   type foo = gated;
+      |        ^--
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-gate2.wit b/crates/wit-parser/tests/ui/parse-fail/bad-gate2.wit
new file mode 100644
index 00000000..fb5d86f1
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/bad-gate2.wit
@@ -0,0 +1,8 @@
+package a:b;
+
+@unstable(feature = not-active)
+world a {}
+
+world b {
+  include a;
+}
diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-gate2.wit.result b/crates/wit-parser/tests/ui/parse-fail/bad-gate2.wit.result
new file mode 100644
index 00000000..6e56c670
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/bad-gate2.wit.result
@@ -0,0 +1,5 @@
+found a reference to a world which is excluded due to its feature not being activated
+     --> tests/ui/parse-fail/bad-gate2.wit:7:11
+      |
+    7 |   include a;
+      |           ^
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-gate3.wit b/crates/wit-parser/tests/ui/parse-fail/bad-gate3.wit
new file mode 100644
index 00000000..3201389f
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/bad-gate3.wit
@@ -0,0 +1,6 @@
+package a:b;
+
+@unstable(feature = not-active)
+interface a {
+  type a = u32;
+}
diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-gate3.wit.result b/crates/wit-parser/tests/ui/parse-fail/bad-gate3.wit.result
new file mode 100644
index 00000000..e8e4df1d
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/bad-gate3.wit.result
@@ -0,0 +1,8 @@
+this type is not gated by a feature but its interface is gated by a feature
+
+Caused by:
+    found a reference to a interface which is excluded due to its feature not being activated
+         --> tests/ui/parse-fail/bad-gate3.wit:5:8
+          |
+        5 |   type a = u32;
+          |        ^
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-gate4.wit b/crates/wit-parser/tests/ui/parse-fail/bad-gate4.wit
new file mode 100644
index 00000000..b2666741
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/bad-gate4.wit
@@ -0,0 +1,8 @@
+package a:b;
+
+interface a {
+  @unstable(feature = inactive)
+  resource a {
+    constructor();
+  }
+}
diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-gate4.wit.result b/crates/wit-parser/tests/ui/parse-fail/bad-gate4.wit.result
new file mode 100644
index 00000000..8b47145a
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/bad-gate4.wit.result
@@ -0,0 +1,8 @@
+failed to update function `[constructor]a`
+
+Caused by:
+    found a reference to a type which is excluded due to its feature not being activated
+         --> tests/ui/parse-fail/bad-gate4.wit:6:5
+          |
+        6 |     constructor();
+          |     ^----------
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-gate5.wit b/crates/wit-parser/tests/ui/parse-fail/bad-gate5.wit
new file mode 100644
index 00000000..7316bd1a
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/bad-gate5.wit
@@ -0,0 +1,11 @@
+package a:b;
+
+interface a {
+  @unstable(feature = inactive)
+  resource a {
+    @unstable(feature = inactive)
+    constructor();
+
+    x: static func();
+  }
+}
diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-gate5.wit.result b/crates/wit-parser/tests/ui/parse-fail/bad-gate5.wit.result
new file mode 100644
index 00000000..df063dd3
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/bad-gate5.wit.result
@@ -0,0 +1,8 @@
+failed to update function `[static]a.x`
+
+Caused by:
+    found a reference to a type which is excluded due to its feature not being activated
+         --> tests/ui/parse-fail/bad-gate5.wit:9:5
+          |
+        9 |     x: static func();
+          |     ^
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-since1.wit b/crates/wit-parser/tests/ui/parse-fail/bad-since1.wit
new file mode 100644
index 00000000..255522bd
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/bad-since1.wit
@@ -0,0 +1,4 @@
+package a:b;
+
+@since
+interface foo1 {}
diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-since1.wit.result b/crates/wit-parser/tests/ui/parse-fail/bad-since1.wit.result
new file mode 100644
index 00000000..9585d62b
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/bad-since1.wit.result
@@ -0,0 +1,5 @@
+expected an identifier or string, found keyword `interface`
+     --> tests/ui/parse-fail/bad-since1.wit:4:1
+      |
+    4 | interface foo1 {}
+      | ^--------
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-since3.wit b/crates/wit-parser/tests/ui/parse-fail/bad-since3.wit
new file mode 100644
index 00000000..17d2b5cb
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/bad-since3.wit
@@ -0,0 +1,5 @@
+package a:b;
+
+// for now `version` must come first
+@since(feature = foo, version = 1.0.0)
+interface foo2 {}
diff --git a/crates/wit-parser/tests/ui/parse-fail/bad-since3.wit.result b/crates/wit-parser/tests/ui/parse-fail/bad-since3.wit.result
new file mode 100644
index 00000000..74c1d175
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/bad-since3.wit.result
@@ -0,0 +1,5 @@
+expected `version`, found `feature`
+     --> tests/ui/parse-fail/bad-since3.wit:4:8
+      |
+    4 | @since(feature = foo, version = 1.0.0)
+      |        ^------
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/conflicting-package.wit.result b/crates/wit-parser/tests/ui/parse-fail/conflicting-package.wit.result
index bdf37086..bffa1fc4 100644
--- a/crates/wit-parser/tests/ui/parse-fail/conflicting-package.wit.result
+++ b/crates/wit-parser/tests/ui/parse-fail/conflicting-package.wit.result
@@ -2,7 +2,8 @@ failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/conf
 
 Caused by:
     0: failed to parse package: tests/ui/parse-fail/conflicting-package
-    1: package identifier `foo:b` does not match previous package name of `foo:a`
+    1: failed to start resolving path: tests/ui/parse-fail/conflicting-package/b.wit
+    2: package identifier `foo:b` does not match previous package name of `foo:a`
             --> tests/ui/parse-fail/conflicting-package/b.wit:1:9
              |
            1 | package foo:b;
diff --git a/crates/wit-parser/tests/ui/parse-fail/multiple-package-docs.wit.result b/crates/wit-parser/tests/ui/parse-fail/multiple-package-docs.wit.result
index 15356332..4ce44238 100644
--- a/crates/wit-parser/tests/ui/parse-fail/multiple-package-docs.wit.result
+++ b/crates/wit-parser/tests/ui/parse-fail/multiple-package-docs.wit.result
@@ -2,7 +2,8 @@ failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/mult
 
 Caused by:
     0: failed to parse package: tests/ui/parse-fail/multiple-package-docs
-    1: found doc comments on multiple 'package' items
+    1: failed to start resolving path: tests/ui/parse-fail/multiple-package-docs/b.wit
+    2: found doc comments on multiple 'package' items
             --> tests/ui/parse-fail/multiple-package-docs/b.wit:1:1
              |
            1 | /// Multiple package docs, B
diff --git a/crates/wit-parser/tests/ui/parse-fail/resources-multiple-returns-borrow.wit.result b/crates/wit-parser/tests/ui/parse-fail/resources-multiple-returns-borrow.wit.result
index 62e78ca1..2094237f 100644
--- a/crates/wit-parser/tests/ui/parse-fail/resources-multiple-returns-borrow.wit.result
+++ b/crates/wit-parser/tests/ui/parse-fail/resources-multiple-returns-borrow.wit.result
@@ -1,5 +1,8 @@
-function returns a type which contains a `borrow<T>` which is not supported
-     --> tests/ui/parse-fail/resources-multiple-returns-borrow.wit:7:5
-      |
-    7 |     f1: func() -> (a: s32, handle: borrow<r1>);
-      |     ^-
\ No newline at end of file
+failed to update function `[method]r1.f1`
+
+Caused by:
+    function returns a type which contains a `borrow<T>` which is not supported
+         --> tests/ui/parse-fail/resources-multiple-returns-borrow.wit:7:5
+          |
+        7 |     f1: func() -> (a: s32, handle: borrow<r1>);
+          |     ^-
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/resources-return-borrow.wit.result b/crates/wit-parser/tests/ui/parse-fail/resources-return-borrow.wit.result
index 3919bf72..79de3292 100644
--- a/crates/wit-parser/tests/ui/parse-fail/resources-return-borrow.wit.result
+++ b/crates/wit-parser/tests/ui/parse-fail/resources-return-borrow.wit.result
@@ -1,5 +1,8 @@
-function returns a type which contains a `borrow<T>` which is not supported
-     --> tests/ui/parse-fail/resources-return-borrow.wit:7:5
-      |
-    7 |     f1: func() -> borrow<r1>;
-      |     ^-
\ No newline at end of file
+failed to update function `[method]r1.f1`
+
+Caused by:
+    function returns a type which contains a `borrow<T>` which is not supported
+         --> tests/ui/parse-fail/resources-return-borrow.wit:7:5
+          |
+        7 |     f1: func() -> borrow<r1>;
+          |     ^-
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/return-borrow1.wit.result b/crates/wit-parser/tests/ui/parse-fail/return-borrow1.wit.result
index aa7dd507..0a0468dd 100644
--- a/crates/wit-parser/tests/ui/parse-fail/return-borrow1.wit.result
+++ b/crates/wit-parser/tests/ui/parse-fail/return-borrow1.wit.result
@@ -1,5 +1,8 @@
-function returns a type which contains a `borrow<T>` which is not supported
-     --> tests/ui/parse-fail/return-borrow1.wit:6:3
-      |
-    6 |   x: func() -> borrow<y>;
-      |   ^
\ No newline at end of file
+failed to update function `x`
+
+Caused by:
+    function returns a type which contains a `borrow<T>` which is not supported
+         --> tests/ui/parse-fail/return-borrow1.wit:6:3
+          |
+        6 |   x: func() -> borrow<y>;
+          |   ^
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/return-borrow2.wit.result b/crates/wit-parser/tests/ui/parse-fail/return-borrow2.wit.result
index d29f0ee7..b46631bb 100644
--- a/crates/wit-parser/tests/ui/parse-fail/return-borrow2.wit.result
+++ b/crates/wit-parser/tests/ui/parse-fail/return-borrow2.wit.result
@@ -1,5 +1,8 @@
-function returns a type which contains a `borrow<T>` which is not supported
-     --> tests/ui/parse-fail/return-borrow2.wit:5:5
-      |
-    5 |     x: func() -> borrow<y>;
-      |     ^
\ No newline at end of file
+failed to update function `[method]y.x`
+
+Caused by:
+    function returns a type which contains a `borrow<T>` which is not supported
+         --> tests/ui/parse-fail/return-borrow2.wit:5:5
+          |
+        5 |     x: func() -> borrow<y>;
+          |     ^
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/return-borrow6.wit.result b/crates/wit-parser/tests/ui/parse-fail/return-borrow6.wit.result
index 4c0f86ab..13d61fba 100644
--- a/crates/wit-parser/tests/ui/parse-fail/return-borrow6.wit.result
+++ b/crates/wit-parser/tests/ui/parse-fail/return-borrow6.wit.result
@@ -1,5 +1,8 @@
-function returns a type which contains a `borrow<T>` which is not supported
-     --> tests/ui/parse-fail/return-borrow6.wit:6:3
-      |
-    6 |   x: func() -> tuple<borrow<y>>;
-      |   ^
\ No newline at end of file
+failed to update function `x`
+
+Caused by:
+    function returns a type which contains a `borrow<T>` which is not supported
+         --> tests/ui/parse-fail/return-borrow6.wit:6:3
+          |
+        6 |   x: func() -> tuple<borrow<y>>;
+          |   ^
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/return-borrow7.wit.result b/crates/wit-parser/tests/ui/parse-fail/return-borrow7.wit.result
index 5d8532f9..3caaf3fe 100644
--- a/crates/wit-parser/tests/ui/parse-fail/return-borrow7.wit.result
+++ b/crates/wit-parser/tests/ui/parse-fail/return-borrow7.wit.result
@@ -1,5 +1,8 @@
-function returns a type which contains a `borrow<T>` which is not supported
-     --> tests/ui/parse-fail/return-borrow7.wit:10:3
-      |
-   10 |   x: func() -> y2;
-      |   ^
\ No newline at end of file
+failed to update function `x`
+
+Caused by:
+    function returns a type which contains a `borrow<T>` which is not supported
+         --> tests/ui/parse-fail/return-borrow7.wit:10:3
+          |
+       10 |   x: func() -> y2;
+          |   ^
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/return-borrow8.wit.result b/crates/wit-parser/tests/ui/parse-fail/return-borrow8.wit.result
index 6382feba..9fd1f027 100644
--- a/crates/wit-parser/tests/ui/parse-fail/return-borrow8.wit.result
+++ b/crates/wit-parser/tests/ui/parse-fail/return-borrow8.wit.result
@@ -1,8 +1,9 @@
 failed to resolve directory while parsing WIT for path [tests/ui/parse-fail/return-borrow8]
 
 Caused by:
-    function returns a type which contains a `borrow<T>` which is not supported
-         --> tests/ui/parse-fail/return-borrow8/foo.wit:6:3
-          |
-        6 |   x: func() -> r;
-          |   ^
\ No newline at end of file
+    0: failed to update function `x`
+    1: function returns a type which contains a `borrow<T>` which is not supported
+            --> tests/ui/parse-fail/return-borrow8/foo.wit:6:3
+             |
+           6 |   x: func() -> r;
+             |   ^
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/shared-types.wit.json b/crates/wit-parser/tests/ui/shared-types.wit.json
index d0dba659..569f6d38 100644
--- a/crates/wit-parser/tests/ui/shared-types.wit.json
+++ b/crates/wit-parser/tests/ui/shared-types.wit.json
@@ -4,12 +4,16 @@
       "name": "foo",
       "imports": {
         "foo": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         }
       },
       "exports": {
         "bar": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         }
       },
       "package": 0
diff --git a/crates/wit-parser/tests/ui/since-and-unstable.wit b/crates/wit-parser/tests/ui/since-and-unstable.wit
new file mode 100644
index 00000000..130d34a6
--- /dev/null
+++ b/crates/wit-parser/tests/ui/since-and-unstable.wit
@@ -0,0 +1,89 @@
+package a:b@1.0.1;
+
+@since(version = 1.0.0)
+interface foo1 {}
+
+@since(version = 1.0.0, feature = foo)
+interface foo2 {}
+
+@since(version = 1.0.0, feature = foo-bar)
+interface foo3 {}
+
+@unstable(feature = foo2)
+interface foo4 {}
+
+@since(version = 1.0.1)
+world w1 {}
+
+@since(version = 1.0.0)
+world w2 {}
+
+interface in-an-interface {
+  @since(version = 1.0.0)
+  foo: func();
+  @since(version = 1.0.0)
+  resource r1;
+  @since(version = 1.0.0)
+  resource r2 {}
+
+  @since(version = 1.0.0)
+  type t1 = u32;
+  @since(version = 1.0.0)
+  record t2 { a: u32 }
+  @since(version = 1.0.0)
+  enum t3 { a }
+  @since(version = 1.0.0)
+  flags t4 { a }
+  @since(version = 1.0.0)
+  variant t5 { a }
+
+  @since(version = 1.0.0)
+  resource r3 {
+    @since(version = 1.0.0)
+    constructor();
+
+    @since(version = 1.0.0)
+    x1: static func();
+
+    @since(version = 1.0.0)
+    x2: func();
+  }
+}
+
+interface z {}
+
+world in-a-world {
+  @since(version = 1.0.0)
+  import x: func();
+  @since(version = 1.0.0)
+  export x: func();
+
+  @since(version = 1.0.0)
+  import y: interface {}
+  @since(version = 1.0.0)
+  export y: interface {}
+
+  @since(version = 1.0.0)
+  import z;
+  @since(version = 1.0.0)
+  export z;
+
+
+  @since(version = 1.0.0)
+  record t1 { x: u32 }
+  @since(version = 1.0.0)
+  enum t2 { a }
+  @since(version = 1.0.0)
+  variant t3 { a }
+  @since(version = 1.0.0)
+  flags t4 { a }
+  @since(version = 1.0.0)
+  type t5 = u32;
+  @since(version = 1.0.0)
+  resource t6;
+  @since(version = 1.0.0)
+  resource t7 {
+    @since(version = 1.0.0)
+    constructor();
+  }
+}
diff --git a/crates/wit-parser/tests/ui/since-and-unstable.wit.json b/crates/wit-parser/tests/ui/since-and-unstable.wit.json
new file mode 100644
index 00000000..c5efc938
--- /dev/null
+++ b/crates/wit-parser/tests/ui/since-and-unstable.wit.json
@@ -0,0 +1,549 @@
+{
+  "worlds": [
+    {
+      "name": "w1",
+      "imports": {},
+      "exports": {},
+      "package": 0,
+      "stability": {
+        "type": "stable",
+        "since": "1.0.1"
+      }
+    },
+    {
+      "name": "w2",
+      "imports": {},
+      "exports": {},
+      "package": 0,
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": "in-a-world",
+      "imports": {
+        "y": {
+          "interface": {
+            "id": 5,
+            "stability": {
+              "type": "stable",
+              "since": "1.0.0"
+            }
+          }
+        },
+        "interface-4": {
+          "interface": {
+            "id": 4,
+            "stability": {
+              "type": "stable",
+              "since": "1.0.0"
+            }
+          }
+        },
+        "t1": {
+          "type": 9
+        },
+        "t2": {
+          "type": 10
+        },
+        "t3": {
+          "type": 11
+        },
+        "t4": {
+          "type": 12
+        },
+        "t5": {
+          "type": 13
+        },
+        "t6": {
+          "type": 14
+        },
+        "t7": {
+          "type": 15
+        },
+        "x": {
+          "function": {
+            "name": "x",
+            "kind": "freestanding",
+            "params": [],
+            "results": [],
+            "stability": {
+              "type": "stable",
+              "since": "1.0.0"
+            }
+          }
+        },
+        "[constructor]t7": {
+          "function": {
+            "name": "[constructor]t7",
+            "kind": {
+              "constructor": 15
+            },
+            "params": [],
+            "results": [
+              {
+                "type": 17
+              }
+            ],
+            "stability": {
+              "type": "stable",
+              "since": "1.0.0"
+            }
+          }
+        }
+      },
+      "exports": {
+        "x": {
+          "function": {
+            "name": "x",
+            "kind": "freestanding",
+            "params": [],
+            "results": [],
+            "stability": {
+              "type": "stable",
+              "since": "1.0.0"
+            }
+          }
+        },
+        "y": {
+          "interface": {
+            "id": 6,
+            "stability": {
+              "type": "stable",
+              "since": "1.0.0"
+            }
+          }
+        },
+        "interface-4": {
+          "interface": {
+            "id": 4,
+            "stability": {
+              "type": "stable",
+              "since": "1.0.0"
+            }
+          }
+        }
+      },
+      "package": 0
+    }
+  ],
+  "interfaces": [
+    {
+      "name": "foo1",
+      "types": {},
+      "functions": {},
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      },
+      "package": 0
+    },
+    {
+      "name": "foo2",
+      "types": {},
+      "functions": {},
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0",
+        "feature": "foo"
+      },
+      "package": 0
+    },
+    {
+      "name": "foo3",
+      "types": {},
+      "functions": {},
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0",
+        "feature": "foo-bar"
+      },
+      "package": 0
+    },
+    {
+      "name": "in-an-interface",
+      "types": {
+        "r1": 0,
+        "r2": 1,
+        "t1": 2,
+        "t2": 3,
+        "t3": 4,
+        "t4": 5,
+        "t5": 6,
+        "r3": 7
+      },
+      "functions": {
+        "foo": {
+          "name": "foo",
+          "kind": "freestanding",
+          "params": [],
+          "results": [],
+          "stability": {
+            "type": "stable",
+            "since": "1.0.0"
+          }
+        },
+        "[constructor]r3": {
+          "name": "[constructor]r3",
+          "kind": {
+            "constructor": 7
+          },
+          "params": [],
+          "results": [
+            {
+              "type": 16
+            }
+          ],
+          "stability": {
+            "type": "stable",
+            "since": "1.0.0"
+          }
+        },
+        "[static]r3.x1": {
+          "name": "[static]r3.x1",
+          "kind": {
+            "static": 7
+          },
+          "params": [],
+          "results": [],
+          "stability": {
+            "type": "stable",
+            "since": "1.0.0"
+          }
+        },
+        "[method]r3.x2": {
+          "name": "[method]r3.x2",
+          "kind": {
+            "method": 7
+          },
+          "params": [
+            {
+              "name": "self",
+              "type": 8
+            }
+          ],
+          "results": [],
+          "stability": {
+            "type": "stable",
+            "since": "1.0.0"
+          }
+        }
+      },
+      "package": 0
+    },
+    {
+      "name": "z",
+      "types": {},
+      "functions": {},
+      "package": 0
+    },
+    {
+      "name": null,
+      "types": {},
+      "functions": {},
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      },
+      "package": 0
+    },
+    {
+      "name": null,
+      "types": {},
+      "functions": {},
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      },
+      "package": 0
+    }
+  ],
+  "types": [
+    {
+      "name": "r1",
+      "kind": "resource",
+      "owner": {
+        "interface": 3
+      },
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": "r2",
+      "kind": "resource",
+      "owner": {
+        "interface": 3
+      },
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": "t1",
+      "kind": {
+        "type": "u32"
+      },
+      "owner": {
+        "interface": 3
+      },
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": "t2",
+      "kind": {
+        "record": {
+          "fields": [
+            {
+              "name": "a",
+              "type": "u32"
+            }
+          ]
+        }
+      },
+      "owner": {
+        "interface": 3
+      },
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": "t3",
+      "kind": {
+        "enum": {
+          "cases": [
+            {
+              "name": "a"
+            }
+          ]
+        }
+      },
+      "owner": {
+        "interface": 3
+      },
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": "t4",
+      "kind": {
+        "flags": {
+          "flags": [
+            {
+              "name": "a"
+            }
+          ]
+        }
+      },
+      "owner": {
+        "interface": 3
+      },
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": "t5",
+      "kind": {
+        "variant": {
+          "cases": [
+            {
+              "name": "a",
+              "type": null
+            }
+          ]
+        }
+      },
+      "owner": {
+        "interface": 3
+      },
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": "r3",
+      "kind": "resource",
+      "owner": {
+        "interface": 3
+      },
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": null,
+      "kind": {
+        "handle": {
+          "borrow": 7
+        }
+      },
+      "owner": null,
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": "t1",
+      "kind": {
+        "record": {
+          "fields": [
+            {
+              "name": "x",
+              "type": "u32"
+            }
+          ]
+        }
+      },
+      "owner": {
+        "world": 2
+      },
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": "t2",
+      "kind": {
+        "enum": {
+          "cases": [
+            {
+              "name": "a"
+            }
+          ]
+        }
+      },
+      "owner": {
+        "world": 2
+      },
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": "t3",
+      "kind": {
+        "variant": {
+          "cases": [
+            {
+              "name": "a",
+              "type": null
+            }
+          ]
+        }
+      },
+      "owner": {
+        "world": 2
+      },
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": "t4",
+      "kind": {
+        "flags": {
+          "flags": [
+            {
+              "name": "a"
+            }
+          ]
+        }
+      },
+      "owner": {
+        "world": 2
+      },
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": "t5",
+      "kind": {
+        "type": "u32"
+      },
+      "owner": {
+        "world": 2
+      },
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": "t6",
+      "kind": "resource",
+      "owner": {
+        "world": 2
+      },
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": "t7",
+      "kind": "resource",
+      "owner": {
+        "world": 2
+      },
+      "stability": {
+        "type": "stable",
+        "since": "1.0.0"
+      }
+    },
+    {
+      "name": null,
+      "kind": {
+        "handle": {
+          "own": 7
+        }
+      },
+      "owner": null
+    },
+    {
+      "name": null,
+      "kind": {
+        "handle": {
+          "own": 15
+        }
+      },
+      "owner": null
+    }
+  ],
+  "packages": [
+    {
+      "name": "a:b@1.0.1",
+      "interfaces": {
+        "foo1": 0,
+        "foo2": 1,
+        "foo3": 2,
+        "in-an-interface": 3,
+        "z": 4
+      },
+      "worlds": {
+        "w1": 0,
+        "w2": 1,
+        "in-a-world": 2
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/stress-export-elaborate.wit.json b/crates/wit-parser/tests/ui/stress-export-elaborate.wit.json
index bd8fdfce..d7781277 100644
--- a/crates/wit-parser/tests/ui/stress-export-elaborate.wit.json
+++ b/crates/wit-parser/tests/ui/stress-export-elaborate.wit.json
@@ -4,36 +4,56 @@
       "name": "foo",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         },
         "interface-2": {
-          "interface": 2
+          "interface": {
+            "id": 2
+          }
         },
         "interface-3": {
-          "interface": 3
+          "interface": {
+            "id": 3
+          }
         },
         "interface-4": {
-          "interface": 4
+          "interface": {
+            "id": 4
+          }
         },
         "interface-5": {
-          "interface": 5
+          "interface": {
+            "id": 5
+          }
         },
         "interface-6": {
-          "interface": 6
+          "interface": {
+            "id": 6
+          }
         },
         "interface-7": {
-          "interface": 7
+          "interface": {
+            "id": 7
+          }
         },
         "interface-8": {
-          "interface": 8
+          "interface": {
+            "id": 8
+          }
         }
       },
       "exports": {
         "interface-9": {
-          "interface": 9
+          "interface": {
+            "id": 9
+          }
         }
       },
       "package": 0
diff --git a/crates/wit-parser/tests/ui/world-diamond.wit.json b/crates/wit-parser/tests/ui/world-diamond.wit.json
index 037141c3..6dba45ed 100644
--- a/crates/wit-parser/tests/ui/world-diamond.wit.json
+++ b/crates/wit-parser/tests/ui/world-diamond.wit.json
@@ -4,13 +4,19 @@
       "name": "the-world",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         },
         "interface-2": {
-          "interface": 2
+          "interface": {
+            "id": 2
+          }
         },
         "a": {
           "function": {
diff --git a/crates/wit-parser/tests/ui/world-iface-no-collide.wit.json b/crates/wit-parser/tests/ui/world-iface-no-collide.wit.json
index b9ad0846..fffc1155 100644
--- a/crates/wit-parser/tests/ui/world-iface-no-collide.wit.json
+++ b/crates/wit-parser/tests/ui/world-iface-no-collide.wit.json
@@ -4,7 +4,9 @@
       "name": "bar",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "t": {
           "type": 1
diff --git a/crates/wit-parser/tests/ui/world-implicit-import1.wit.json b/crates/wit-parser/tests/ui/world-implicit-import1.wit.json
index f0b550b7..708c339b 100644
--- a/crates/wit-parser/tests/ui/world-implicit-import1.wit.json
+++ b/crates/wit-parser/tests/ui/world-implicit-import1.wit.json
@@ -4,13 +4,19 @@
       "name": "the-world",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "bar": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         },
         "foo": {
-          "interface": 2
+          "interface": {
+            "id": 2
+          }
         }
       },
       "exports": {},
diff --git a/crates/wit-parser/tests/ui/world-implicit-import2.wit.json b/crates/wit-parser/tests/ui/world-implicit-import2.wit.json
index 84e150e4..97049842 100644
--- a/crates/wit-parser/tests/ui/world-implicit-import2.wit.json
+++ b/crates/wit-parser/tests/ui/world-implicit-import2.wit.json
@@ -4,7 +4,9 @@
       "name": "w",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "g": {
           "type": 1
diff --git a/crates/wit-parser/tests/ui/world-implicit-import3.wit.json b/crates/wit-parser/tests/ui/world-implicit-import3.wit.json
index 3e716e41..159bee77 100644
--- a/crates/wit-parser/tests/ui/world-implicit-import3.wit.json
+++ b/crates/wit-parser/tests/ui/world-implicit-import3.wit.json
@@ -4,7 +4,9 @@
       "name": "w",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "g": {
           "type": 1
diff --git a/crates/wit-parser/tests/ui/world-same-fields4.wit.json b/crates/wit-parser/tests/ui/world-same-fields4.wit.json
index c15baf18..3be16acf 100644
--- a/crates/wit-parser/tests/ui/world-same-fields4.wit.json
+++ b/crates/wit-parser/tests/ui/world-same-fields4.wit.json
@@ -4,15 +4,21 @@
       "name": "foo",
       "imports": {
         "shared-items": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         },
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         }
       },
       "exports": {
         "a-name": {
-          "interface": 2
+          "interface": {
+            "id": 2
+          }
         }
       },
       "package": 0
diff --git a/crates/wit-parser/tests/ui/world-top-level-resources.wit.json b/crates/wit-parser/tests/ui/world-top-level-resources.wit.json
index a4ef0416..794e699f 100644
--- a/crates/wit-parser/tests/ui/world-top-level-resources.wit.json
+++ b/crates/wit-parser/tests/ui/world-top-level-resources.wit.json
@@ -4,15 +4,21 @@
       "name": "proxy",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         }
       },
       "exports": {
         "interface-1": {
-          "interface": 1
+          "interface": {
+            "id": 1
+          }
         }
       },
       "package": 0
diff --git a/crates/wit-parser/tests/ui/worlds-union-dedup.wit.json b/crates/wit-parser/tests/ui/worlds-union-dedup.wit.json
index 7fb8ff0a..d1e24e97 100644
--- a/crates/wit-parser/tests/ui/worlds-union-dedup.wit.json
+++ b/crates/wit-parser/tests/ui/worlds-union-dedup.wit.json
@@ -4,10 +4,14 @@
       "name": "my-world-a",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "interface-2": {
-          "interface": 2
+          "interface": {
+            "id": 2
+          }
         }
       },
       "exports": {},
@@ -17,10 +21,14 @@
       "name": "my-world-b",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "interface-2": {
-          "interface": 2
+          "interface": {
+            "id": 2
+          }
         }
       },
       "exports": {},
@@ -30,10 +38,14 @@
       "name": "union-my-world",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "interface-2": {
-          "interface": 2
+          "interface": {
+            "id": 2
+          }
         }
       },
       "exports": {},
diff --git a/crates/wit-parser/tests/ui/worlds-with-types.wit.json b/crates/wit-parser/tests/ui/worlds-with-types.wit.json
index 9ca60c0a..70e857d6 100644
--- a/crates/wit-parser/tests/ui/worlds-with-types.wit.json
+++ b/crates/wit-parser/tests/ui/worlds-with-types.wit.json
@@ -35,7 +35,9 @@
       "name": "bar",
       "imports": {
         "interface-0": {
-          "interface": 0
+          "interface": {
+            "id": 0
+          }
         },
         "t": {
           "type": 3
diff --git a/tests/cli/wit-stability-in-binary-format.wit b/tests/cli/wit-stability-in-binary-format.wit
new file mode 100644
index 00000000..73a33b27
--- /dev/null
+++ b/tests/cli/wit-stability-in-binary-format.wit
@@ -0,0 +1,39 @@
+// RUN: component wit % --wasm | component wit
+
+package a:b;
+
+@since(version = 1.0.0)
+interface foo {
+  @since(version = 1.0.0)
+  type t = u32;
+
+  @since(version = 1.0.0)
+  f: func();
+
+  @since(version = 1.0.0)
+  resource r {
+    @since(version = 1.0.0)
+    constructor();
+  }
+}
+
+@since(version = 1.0.0)
+world w {
+  @since(version = 1.0.0)
+  import foo;
+  @since(version = 1.0.0)
+  export foo;
+
+  @since(version = 1.0.0)
+  import f: func();
+  @since(version = 1.0.0)
+  export f: func();
+
+  @since(version = 1.0.0)
+  type t = u32;
+
+  @since(version = 1.0.0)
+  import a: interface {}
+  @since(version = 1.0.0)
+  export a: interface {}
+}
diff --git a/tests/cli/wit-stability-in-binary-format.wit.stdout b/tests/cli/wit-stability-in-binary-format.wit.stdout
new file mode 100644
index 00000000..f3191af2
--- /dev/null
+++ b/tests/cli/wit-stability-in-binary-format.wit.stdout
@@ -0,0 +1,39 @@
+/// RUN: component wit % --wasm | component wit
+package a:b;
+
+@since(version = 1.0.0)
+interface foo {
+  @since(version = 1.0.0)
+  type t = u32;
+
+  @since(version = 1.0.0)
+  resource r {
+    @since(version = 1.0.0)
+    constructor();
+  }
+
+  @since(version = 1.0.0)
+  f: func();
+}
+
+@since(version = 1.0.0)
+world w {
+  @since(version = 1.0.0)
+  import foo;
+  @since(version = 1.0.0)
+  import a: interface {
+  }
+  @since(version = 1.0.0)
+  import f: func();
+
+  @since(version = 1.0.0)
+  type t = u32;
+
+  @since(version = 1.0.0)
+  export f: func();
+  @since(version = 1.0.0)
+  export foo;
+  @since(version = 1.0.0)
+  export a: interface {
+  }
+}
diff --git a/tests/cli/wit-stability-inherited.wit b/tests/cli/wit-stability-inherited.wit
new file mode 100644
index 00000000..572e1c25
--- /dev/null
+++ b/tests/cli/wit-stability-inherited.wit
@@ -0,0 +1,25 @@
+// RUN: component wit %
+
+package a:b;
+
+interface foo {
+  type t = u32;
+}
+
+interface bar {
+  use foo.{t};
+}
+
+interface baz {
+  use bar.{t};
+}
+
+world x {
+  @since(version = 1.0.0)
+  import baz;
+}
+
+world y {
+  @since(version = 1.0.0)
+  export baz;
+}
diff --git a/tests/cli/wit-stability-inherited.wit.stdout b/tests/cli/wit-stability-inherited.wit.stdout
new file mode 100644
index 00000000..651b90e2
--- /dev/null
+++ b/tests/cli/wit-stability-inherited.wit.stdout
@@ -0,0 +1,32 @@
+/// RUN: component wit %
+package a:b;
+
+interface foo {
+  type t = u32;
+}
+
+interface bar {
+  use foo.{t};
+}
+
+interface baz {
+  use bar.{t};
+}
+
+world x {
+  @since(version = 1.0.0)
+  import foo;
+  @since(version = 1.0.0)
+  import bar;
+  @since(version = 1.0.0)
+  import baz;
+}
+world y {
+  @since(version = 1.0.0)
+  import foo;
+  @since(version = 1.0.0)
+  import bar;
+
+  @since(version = 1.0.0)
+  export baz;
+}
diff --git a/tests/cli/wit-with-features.wit b/tests/cli/wit-with-features.wit
new file mode 100644
index 00000000..1db1d916
--- /dev/null
+++ b/tests/cli/wit-with-features.wit
@@ -0,0 +1,56 @@
+// RUN: component wit --features foo %
+
+package a:b;
+
+@unstable(feature = foo)
+interface foo {
+  @unstable(feature = foo)
+  type t = u32;
+}
+
+@unstable(feature = foo)
+interface bar {
+  @unstable(feature = foo)
+  use foo.{t};
+
+  @unstable(feature = foo)
+  record x { y: t }
+}
+
+@unstable(feature = foo)
+world the-world {
+  @unstable(feature = foo)
+  import foo;
+  @unstable(feature = foo)
+  export bar;
+
+  @unstable(feature = foo)
+  import x: func();
+  @unstable(feature = foo)
+  export y: func();
+
+  @unstable(feature = foo)
+  resource thing {
+    @unstable(feature = foo)
+    constructor();
+  }
+}
+
+interface another-interface {
+  @unstable(feature = foo)
+  resource x {
+    @unstable(feature = foo)
+    constructor();
+
+    @unstable(feature = foo)
+    x: static func();
+
+    @unstable(feature = foo)
+    y: func();
+  }
+
+  @unstable(feature = foo)
+  variant y {
+    x(x),
+  }
+}
diff --git a/tests/cli/wit-with-features.wit.stdout b/tests/cli/wit-with-features.wit.stdout
new file mode 100644
index 00000000..b402b6aa
--- /dev/null
+++ b/tests/cli/wit-with-features.wit.stdout
@@ -0,0 +1,55 @@
+/// RUN: component wit --features foo %
+package a:b;
+
+@unstable(feature = foo)
+interface foo {
+  @unstable(feature = foo)
+  type t = u32;
+}
+
+@unstable(feature = foo)
+interface bar {
+  @unstable(feature = foo)
+  use foo.{t};
+
+  @unstable(feature = foo)
+  record x {
+    y: t,
+  }
+}
+
+interface another-interface {
+  @unstable(feature = foo)
+  resource x {
+    @unstable(feature = foo)
+    constructor();
+    @unstable(feature = foo)
+    x: static func();
+    @unstable(feature = foo)
+    y: func();
+  }
+
+  @unstable(feature = foo)
+  variant y {
+    x(x),
+  }
+}
+
+@unstable(feature = foo)
+world the-world {
+  @unstable(feature = foo)
+  import foo;
+  @unstable(feature = foo)
+  import x: func();
+
+  @unstable(feature = foo)
+  resource thing {
+    @unstable(feature = foo)
+    constructor();
+  }
+
+  @unstable(feature = foo)
+  export y: func();
+  @unstable(feature = foo)
+  export bar;
+}