Repository: bytecodealliance/wasm-tools (rust)

Issue:
Tolerate duplicate imports when creating components
Multiple imports of the same name are in-spec for core-wasm modules but [not allowed for components](https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#import-and-export-definitions). Currently, attempting to componentize a module with non-uniquely named imports yields…

```
% wasm-tools component new main-with-dupe-imports.wasm -o component-with-dupes.wasm --adapt wasi_snapshot_preview1.wasm
error: failed to encode a component from module

Caused by:
    0: failed to decode world from module
    1: module was not valid
    2: module has duplicate import for `wasi_snapshot_preview1::fd_write`
```

We should pick a unique import of each name and ignore the rest when componentizing. Since inter-component linking is done solely by name, multiple same-named imports have no way of being distinguished in order to be satisfied individually—even if they had unique types, for example—and so nothing is lost by ignoring them.

Pull request description:
Support componentizing modules that have duplicate imports.
When the `--deduplicate-imports` option is passed to `wasm-tools component new`, we rewrite the input module to compact out the duplicates. This allows tinyGo's output to be wrapped in a component. Closes #2063.

Using `Reencode` takes the maintenance pressure I feared off the deduplication engine; it won't have to chase future changes to the wasm format. Instead, `Reencode` should adapt to them and continue calling `function_index()` in all the right places.

_Suggestions on a better place to put wasi_snapshot_preview1.wasm adapter are welcome._

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index a540e3bf..22ffd00c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -334,7 +334,7 @@ dependencies = [
 name = "component"
 version = "0.0.0"
 dependencies = [
- "wasmprinter 0.229.0",
+ "wasmprinter 0.230.0",
  "wat",
  "wit-bindgen-rt 0.26.0",
 ]
@@ -1882,7 +1882,7 @@ dependencies = [
 
 [[package]]
 name = "wasm-compose"
-version = "0.229.0"
+version = "0.230.0"
 dependencies = [
  "anyhow",
  "glob",
@@ -1896,11 +1896,11 @@ dependencies = [
  "serde_derive",
  "serde_yaml",
  "smallvec",
- "wasm-encoder 0.229.0",
- "wasmparser 0.229.0",
- "wasmprinter 0.229.0",
+ "wasm-encoder 0.230.0",
+ "wasmparser 0.230.0",
+ "wasmprinter 0.230.0",
  "wat",
- "wit-component 0.229.0",
+ "wit-component 0.230.0",
 ]
 
 [[package]]
@@ -1925,13 +1925,13 @@ dependencies = [
 
 [[package]]
 name = "wasm-encoder"
-version = "0.229.0"
+version = "0.230.0"
 dependencies = [
  "anyhow",
  "leb128fmt",
  "tempfile",
- "wasmparser 0.229.0",
- "wasmprinter 0.229.0",
+ "wasmparser 0.230.0",
+ "wasmprinter 0.230.0",
 ]
 
 [[package]]
@@ -1952,7 +1952,7 @@ dependencies = [
 
 [[package]]
 name = "wasm-metadata"
-version = "0.229.0"
+version = "0.230.0"
 dependencies = [
  "anyhow",
  "auditable-serde",
@@ -1964,13 +1964,13 @@ dependencies = [
  "serde_json",
  "spdx",
  "url",
- "wasm-encoder 0.229.0",
- "wasmparser 0.229.0",
+ "wasm-encoder 0.230.0",
+ "wasmparser 0.230.0",
 ]
 
 [[package]]
 name = "wasm-mutate"
-version = "0.229.0"
+version = "0.230.0"
 dependencies = [
  "anyhow",
  "clap",
@@ -1979,9 +1979,9 @@ dependencies = [
  "log",
  "rand",
  "thiserror",
- "wasm-encoder 0.229.0",
- "wasmparser 0.229.0",
- "wasmprinter 0.229.0",
+ "wasm-encoder 0.230.0",
+ "wasmparser 0.230.0",
+ "wasmprinter 0.230.0",
  "wat",
 ]
 
@@ -1998,14 +1998,14 @@ dependencies = [
  "num_cpus",
  "rand",
  "wasm-mutate",
- "wasmparser 0.229.0",
- "wasmprinter 0.229.0",
+ "wasmparser 0.230.0",
+ "wasmprinter 0.230.0",
  "wasmtime",
 ]
 
 [[package]]
 name = "wasm-shrink"
-version = "0.229.0"
+version = "0.230.0"
 dependencies = [
  "anyhow",
  "blake3",
@@ -2014,14 +2014,14 @@ dependencies = [
  "log",
  "rand",
  "wasm-mutate",
- "wasmparser 0.229.0",
- "wasmprinter 0.229.0",
+ "wasmparser 0.230.0",
+ "wasmprinter 0.230.0",
  "wat",
 ]
 
 [[package]]
 name = "wasm-smith"
-version = "0.229.0"
+version = "0.230.0"
 dependencies = [
  "anyhow",
  "arbitrary",
@@ -2032,15 +2032,15 @@ dependencies = [
  "rand",
  "serde",
  "serde_derive",
- "wasm-encoder 0.229.0",
- "wasmparser 0.229.0",
- "wasmprinter 0.229.0",
+ "wasm-encoder 0.230.0",
+ "wasmparser 0.230.0",
+ "wasmprinter 0.230.0",
  "wat",
 ]
 
 [[package]]
 name = "wasm-tools"
-version = "1.229.0"
+version = "1.230.0"
 dependencies = [
  "addr2line",
  "anyhow",
@@ -2067,18 +2067,18 @@ dependencies = [
  "tempfile",
  "termcolor",
  "wasm-compose",
- "wasm-encoder 0.229.0",
- "wasm-metadata 0.229.0",
+ "wasm-encoder 0.230.0",
+ "wasm-metadata 0.230.0",
  "wasm-mutate",
  "wasm-shrink",
  "wasm-smith",
- "wasmparser 0.229.0",
- "wasmprinter 0.229.0",
+ "wasmparser 0.230.0",
+ "wasmprinter 0.230.0",
  "wast",
  "wat",
- "wit-component 0.229.0",
+ "wit-component 0.230.0",
  "wit-encoder",
- "wit-parser 0.229.0",
+ "wit-parser 0.230.0",
  "wit-smith",
 ]
 
@@ -2090,8 +2090,8 @@ dependencies = [
  "wasm-mutate",
  "wasm-shrink",
  "wasm-smith",
- "wasmparser 0.229.0",
- "wasmprinter 0.229.0",
+ "wasmparser 0.230.0",
+ "wasmprinter 0.230.0",
  "wast",
  "wat",
 ]
@@ -2107,30 +2107,30 @@ dependencies = [
  "log",
  "rand",
  "tempfile",
- "wasm-encoder 0.229.0",
+ "wasm-encoder 0.230.0",
  "wasm-mutate",
  "wasm-smith",
- "wasmparser 0.229.0",
- "wasmprinter 0.229.0",
+ "wasmparser 0.230.0",
+ "wasmprinter 0.230.0",
  "wasmtime",
  "wast",
  "wat",
  "wit-component 0.214.0",
- "wit-component 0.229.0",
+ "wit-component 0.230.0",
  "wit-parser 0.214.0",
- "wit-parser 0.229.0",
+ "wit-parser 0.230.0",
  "wit-smith",
 ]
 
 [[package]]
 name = "wasm-wave"
-version = "0.229.0"
+version = "0.230.0"
 dependencies = [
  "anyhow",
  "indexmap 2.7.1",
  "logos",
  "thiserror",
- "wit-parser 0.229.0",
+ "wit-parser 0.230.0",
 ]
 
 [[package]]
@@ -2161,7 +2161,7 @@ dependencies = [
 
 [[package]]
 name = "wasmparser"
-version = "0.229.0"
+version = "0.230.0"
 dependencies = [
  "anyhow",
  "bitflags",
@@ -2174,7 +2174,7 @@ dependencies = [
  "rayon",
  "semver",
  "serde",
- "wasm-encoder 0.229.0",
+ "wasm-encoder 0.230.0",
  "wast",
  "wat",
 ]
@@ -2192,11 +2192,11 @@ dependencies = [
 
 [[package]]
 name = "wasmprinter"
-version = "0.229.0"
+version = "0.230.0"
 dependencies = [
  "anyhow",
  "termcolor",
- "wasmparser 0.229.0",
+ "wasmparser 0.230.0",
  "wat",
 ]
 
@@ -2410,7 +2410,7 @@ dependencies = [
 
 [[package]]
 name = "wast"
-version = "229.0.0"
+version = "230.0.0"
 dependencies = [
  "anyhow",
  "bumpalo",
@@ -2420,14 +2420,14 @@ dependencies = [
  "memchr",
  "rand",
  "unicode-width",
- "wasm-encoder 0.229.0",
- "wasmparser 0.229.0",
+ "wasm-encoder 0.230.0",
+ "wasmparser 0.230.0",
  "wat",
 ]
 
 [[package]]
 name = "wat"
-version = "1.229.0"
+version = "1.230.0"
 dependencies = [
  "wast",
 ]
@@ -2593,7 +2593,7 @@ dependencies = [
 
 [[package]]
 name = "wit-component"
-version = "0.229.0"
+version = "0.230.0"
 dependencies = [
  "anyhow",
  "bitflags",
@@ -2606,19 +2606,19 @@ dependencies = [
  "serde",
  "serde_derive",
  "serde_json",
- "wasm-encoder 0.229.0",
- "wasm-metadata 0.229.0",
- "wasmparser 0.229.0",
- "wasmprinter 0.229.0",
+ "wasm-encoder 0.230.0",
+ "wasm-metadata 0.230.0",
+ "wasmparser 0.230.0",
+ "wasmprinter 0.230.0",
  "wasmtime",
  "wast",
  "wat",
- "wit-parser 0.229.0",
+ "wit-parser 0.230.0",
 ]
 
 [[package]]
 name = "wit-encoder"
-version = "0.229.0"
+version = "0.230.0"
 dependencies = [
  "anyhow",
  "id-arena",
@@ -2626,7 +2626,7 @@ dependencies = [
  "pretty_assertions",
  "semver",
  "serde",
- "wit-parser 0.229.0",
+ "wit-parser 0.230.0",
 ]
 
 [[package]]
@@ -2667,7 +2667,7 @@ dependencies = [
 
 [[package]]
 name = "wit-parser"
-version = "0.229.0"
+version = "0.230.0"
 dependencies = [
  "anyhow",
  "env_logger",
@@ -2681,9 +2681,9 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "unicode-xid",
- "wasmparser 0.229.0",
+ "wasmparser 0.230.0",
  "wat",
- "wit-parser 0.229.0",
+ "wit-parser 0.230.0",
 ]
 
 [[package]]
@@ -2694,21 +2694,21 @@ dependencies = [
  "env_logger",
  "libfuzzer-sys",
  "log",
- "wasmprinter 0.229.0",
- "wit-parser 0.229.0",
+ "wasmprinter 0.230.0",
+ "wit-parser 0.230.0",
 ]
 
 [[package]]
 name = "wit-smith"
-version = "0.229.0"
+version = "0.230.0"
 dependencies = [
  "arbitrary",
  "clap",
  "indexmap 2.7.1",
  "log",
  "semver",
- "wit-component 0.229.0",
- "wit-parser 0.229.0",
+ "wit-component 0.230.0",
+ "wit-parser 0.230.0",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index b0b1736a..bd6ebb5c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "wasm-tools"
-version = "1.229.0"
+version = "1.230.0"
 authors = ["The Wasmtime Project Developers"]
 edition.workspace = true
 description = "CLI tools for interoperating with WebAssembly files"
@@ -85,7 +85,7 @@ allow_attributes_without_reason = 'warn'
 [workspace.package]
 edition = '2021'
 license = "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT"
-version = "0.229.0"
+version = "0.230.0"
 # Current policy for wasm-tools is the same as Wasmtime which is that this
 # number can be no larger than the current stable release of Rust minus 2.
 #
@@ -128,20 +128,20 @@ termcolor = "1.2.0"
 url = "2.0.0"
 wasmtime = { version = "29.0.1", default-features = false, features = ['cranelift', 'component-model', 'runtime', 'gc-drc'] }
 
-wasm-compose = { version = "0.229.0", path = "crates/wasm-compose" }
-wasm-encoder = { version = "0.229.0", path = "crates/wasm-encoder", default-features = false }
-wasm-metadata = { version = "0.229.0", path = "crates/wasm-metadata", default-features = false }
-wasm-mutate = { version = "0.229.0", path = "crates/wasm-mutate" }
-wasm-shrink = { version = "0.229.0", path = "crates/wasm-shrink" }
-wasm-smith = { version = "0.229.0", path = "crates/wasm-smith" }
-wasmparser = { version = "0.229.0", path = "crates/wasmparser", default-features = false, features = ['simd'] }
-wasmprinter = { version = "0.229.0", path = "crates/wasmprinter", default-features = false }
-wast = { version = "229.0.0", path = "crates/wast", default-features = false }
-wat = { version = "1.229.0", path = "crates/wat", default-features = false }
-wit-component = { version = "0.229.0", path = "crates/wit-component" }
-wit-encoder = { version = "0.229.0", path = "crates/wit-encoder" }
-wit-parser = { version = "0.229.0", path = "crates/wit-parser" }
-wit-smith = { version = "0.229.0", path = "crates/wit-smith" }
+wasm-compose = { version = "0.230.0", path = "crates/wasm-compose" }
+wasm-encoder = { version = "0.230.0", path = "crates/wasm-encoder", default-features = false }
+wasm-metadata = { version = "0.230.0", path = "crates/wasm-metadata", default-features = false }
+wasm-mutate = { version = "0.230.0", path = "crates/wasm-mutate" }
+wasm-shrink = { version = "0.230.0", path = "crates/wasm-shrink" }
+wasm-smith = { version = "0.230.0", path = "crates/wasm-smith" }
+wasmparser = { version = "0.230.0", path = "crates/wasmparser", default-features = false, features = ['simd'] }
+wasmprinter = { version = "0.230.0", path = "crates/wasmprinter", default-features = false }
+wast = { version = "230.0.0", path = "crates/wast", default-features = false }
+wat = { version = "1.230.0", path = "crates/wat", default-features = false }
+wit-component = { version = "0.230.0", path = "crates/wit-component" }
+wit-encoder = { version = "0.230.0", path = "crates/wit-encoder" }
+wit-parser = { version = "0.230.0", path = "crates/wit-parser" }
+wit-smith = { version = "0.230.0", path = "crates/wit-smith" }
 
 [dependencies]
 anyhow = { workspace = true }
diff --git a/crates/wasm-encoder/src/reencode.rs b/crates/wasm-encoder/src/reencode.rs
index 04a32e69..d3203208 100644
--- a/crates/wasm-encoder/src/reencode.rs
+++ b/crates/wasm-encoder/src/reencode.rs
@@ -554,9 +554,9 @@ pub enum Error<E = Infallible> {
     InvalidConstExpr,
     /// The code section size listed was not valid for the wasm binary provided.
     InvalidCodeSectionSize,
-    /// There was a section that does not belong into a core wasm module.
+    /// There was a section that does not belong in a core wasm module.
     UnexpectedNonCoreModuleSection,
-    /// There was a section that does not belong into a component module.
+    /// There was a section that does not belong in a component module.
     UnexpectedNonComponentSection,
     /// A core type definition was found in a component that's not supported.
     UnsupportedCoreTypeInComponent,
@@ -582,11 +582,11 @@ impl<E: core::fmt::Display> core::fmt::Display for Error<E> {
             Self::InvalidConstExpr => write!(fmt, "The const expression was invalid"),
             Self::UnexpectedNonCoreModuleSection => write!(
                 fmt,
-                "There was a section that does not belong into a core wasm module"
+                "There was a section that does not belong in a core wasm module"
             ),
             Self::UnexpectedNonComponentSection => write!(
                 fmt,
-                "There was a section that does not belong into a component"
+                "There was a section that does not belong in a component"
             ),
             Self::CanonicalizedHeapTypeReference => write!(
                 fmt,
diff --git a/crates/wast/Cargo.toml b/crates/wast/Cargo.toml
index 045b67f7..553b4292 100644
--- a/crates/wast/Cargo.toml
+++ b/crates/wast/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "wast"
-version = "229.0.0"
+version = "230.0.0"
 authors = ["Alex Crichton <alex@alexcrichton.com>"]
 edition.workspace = true
 license.workspace = true
diff --git a/crates/wat/Cargo.toml b/crates/wat/Cargo.toml
index ae3b70ad..6273c726 100644
--- a/crates/wat/Cargo.toml
+++ b/crates/wat/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "wat"
-version = "1.229.0"
+version = "1.230.0"
 authors = ["Alex Crichton <alex@alexcrichton.com>"]
 edition.workspace = true
 license.workspace = true
diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs
index f93d9a7a..e2e40e8f 100644
--- a/crates/wit-component/src/encoding.rs
+++ b/crates/wit-component/src/encoding.rs
@@ -98,6 +98,9 @@ use types::{InstanceTypeEncoder, RootTypeEncoder, ValtypeEncoder};
 mod world;
 use world::{ComponentWorld, ImportedInterface, Lowering};
 
+mod dedupe;
+use dedupe::dedupe_imports;
+
 fn to_val_type(ty: &WasmType) -> ValType {
     match ty {
         WasmType::I32 => ValType::I32,
@@ -2677,6 +2680,7 @@ pub struct ComponentEncoder {
     realloc_via_memory_grow: bool,
     merge_imports_based_on_semver: Option<bool>,
     pub(super) reject_legacy_names: bool,
+    deduplicate_imports: bool,
 }
 
 impl ComponentEncoder {
@@ -2686,7 +2690,12 @@ impl ComponentEncoder {
     /// It will also add any producers information inside the component type information to the
     /// core module.
     pub fn module(mut self, module: &[u8]) -> Result<Self> {
-        let (wasm, metadata) = self.decode(module)?;
+        let (wasm, metadata) = self.decode(module.as_ref())?;
+        let wasm = if self.deduplicate_imports {
+            dedupe_imports(wasm.as_ref())?
+        } else {
+            wasm
+        };
         let exports = self
             .merge_metadata(metadata)
             .context("failed merge WIT metadata for module with previous metadata")?;
@@ -2742,6 +2751,19 @@ impl ComponentEncoder {
         self
     }
 
+    /// Sets whether to remove duplicate function imports to support turning
+    /// otherwise-illegal core modules into components.
+    ///
+    /// While multiple imports of the same module/name pair are legal in core
+    /// modules, they are illegal in components.
+    ///
+    /// This is disabled by default due to the quiet stripping of possibly
+    /// invalidated custom sections.
+    pub fn deduplicate_imports(mut self, dedupe: bool) -> Self {
+        self.deduplicate_imports = dedupe;
+        self
+    }
+
     /// Specifies a new adapter which is used to translate from a historical
     /// wasm ABI to the canonical ABI and the `interface` provided.
     ///
diff --git a/crates/wit-component/src/encoding/dedupe.rs b/crates/wit-component/src/encoding/dedupe.rs
new file mode 100644
index 00000000..1cd81e8f
--- /dev/null
+++ b/crates/wit-component/src/encoding/dedupe.rs
@@ -0,0 +1,316 @@
+//! Routines to rewrite a wasm core module to remove duplicate imports, which
+//! are illegal in components
+//!
+//! Thus far, this supports only duplicatively named *functions*, because our
+//! goal is to make `wasm-tools component new` work, and the only imports that
+//! supports are functions from adapters.
+
+use anyhow::{anyhow, Error};
+use std::{
+    borrow::Cow,
+    collections::hash_map::{Entry, HashMap},
+    fmt::{self, Display, Formatter},
+};
+use wasm_encoder::{
+    self,
+    reencode::{utils::parse_custom_section, Reencode},
+};
+use wasmparser::{self, BinaryReaderError, Import, KnownCustom, Parser, TypeRef};
+
+/// The module/name pair of a wasm import
+#[derive(Hash, Eq, PartialEq, Debug)]
+struct ImportPath {
+    module: String,
+    field: String,
+}
+
+/// Info which lets us quickly replace references to duplicate-imported
+/// functions with refs to the canonical ones once the duplicates have been
+/// removed
+///
+/// Default represents a state as if the import section were entirely absent
+/// from the module.
+#[derive(Default)]
+struct Remappings {
+    /// Map of old func indices to new
+    indices: Vec<u32>,
+    /// Number of duplicate imports removed
+    num_dupes: u32,
+    /// Which imports indices were duplicates
+    dupes: Vec<bool>,
+}
+
+impl Remappings {
+    /// Works out the mappings of a core module's func idxs to new ones that
+    /// take into account the compacting-out of duplicate imports. The indices
+    /// can then be looked up using `new_index_for()`.
+    ///
+    /// Calling this twice replaces any previous state iff it returns `Ok`.
+    fn imports<'a, T>(&'a mut self, import_section: T) -> Result<(), BinaryReaderError>
+    where
+        T: IntoIterator<Item = Result<Import<'a>, BinaryReaderError>>,
+    {
+        let mut num_dupes = 0;
+        // A map of module/field pairs to the first import idx at which each
+        // occurs
+        let mut canonicals = HashMap::new();
+        let mut indices = Vec::new();
+        let mut dupes = Vec::new();
+
+        // import_section.clone() lets us iterate over the section a 2nd time
+        // without consuming the original iterator or copying the bytes.
+        let mut func_idx = 0u32; // Imports are a "vec" of imports and thus bounded to u32.
+        for import in import_section.into_iter() {
+            let import = import?;
+            let TypeRef::Func(_) = import.ty else {
+                dupes.push(false);
+                continue;
+            };
+            let path = ImportPath {
+                module: import.module.to_string(),
+                field: import.name.to_string(),
+            };
+            match canonicals.entry(path) {
+                Entry::Occupied(slot) => {
+                    indices.push(*slot.get());
+                    num_dupes += 1;
+                    dupes.push(true);
+                }
+                Entry::Vacant(slot) => {
+                    indices.push(*slot.insert(func_idx - num_dupes));
+                    dupes.push(false);
+                }
+            }
+            func_idx += 1;
+        }
+        self.indices = indices;
+        self.num_dupes = num_dupes;
+        self.dupes = dupes;
+        Ok(())
+    }
+
+    /// Given an original-to-the-module func index, returns the one that should
+    /// be used once duplicate imports are removed.
+    fn new_index_for(&self, idx: u32) -> u32 {
+        match self.indices.get(idx as usize) {
+            // Return a remapping using the index vector, if the idx is within
+            // bounds of the imports.
+            Some(new_idx) => *new_idx,
+            // Otherwise, it's a local idx (as from the local function section),
+            // and we need only subtract the number of duplicates removed. (The
+            // spec says "The index space for functions, tables, memories and
+            // globals includes respective imports declared in the same module.
+            // The indices of these imports precede the indices of other
+            // definitions in the same index space.")
+            None => idx - self.num_dupes,
+        }
+    }
+
+    /// Returns whether we contain no remappings to apply.
+    fn is_empty(&self) -> bool {
+        self.num_dupes == 0
+    }
+
+    /// Returns whether the import with the given index has been eliminated and
+    /// references to it redirected to a preceding one.
+    fn is_duplicate(&self, import_idx: u32) -> bool {
+        match self.dupes.get(import_idx as usize) {
+            Some(b) => *b,
+            None => false,
+        }
+    }
+}
+
+/// No duplicate imports were found, so I have nothing to do.
+struct NoDuplicatesFound;
+
+impl Display for NoDuplicatesFound {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "no duplicate functions were found, so there was nothing to do"
+        )
+    }
+}
+
+#[derive(Default)]
+struct DedupingReencoder {
+    remappings: Remappings,
+}
+
+type DeduperError = wasm_encoder::reencode::Error<NoDuplicatesFound>;
+
+impl Reencode for DedupingReencoder {
+    type Error = NoDuplicatesFound;
+
+    // Fortunately, the import section comes before any section we need to
+    // modify. Thus, we don't have to decode the wasm twice.
+    fn parse_import_section(
+        &mut self,
+        imports: &mut wasm_encoder::ImportSection,
+        section: wasmparser::ImportSectionReader<'_>,
+    ) -> Result<(), wasm_encoder::reencode::Error<Self::Error>> {
+        self.remappings.imports(section.clone())?;
+
+        // If no duplicates, take the fast path out.
+        if self.remappings.is_empty() {
+            return Err(DeduperError::UserError(NoDuplicatesFound));
+        }
+
+        for (idx, import) in section.into_iter().enumerate() {
+            if !self.remappings.is_duplicate(idx as u32) {
+                self.parse_import(imports, import?)?;
+            }
+        }
+        Ok(())
+    }
+
+    fn function_index(&mut self, func_idx: u32) -> u32 {
+        self.remappings.new_index_for(func_idx)
+    }
+
+    // Strip all but known-safe custom sections.
+    //
+    // Because custom sections are opaque to us, they may contain information
+    // (like debugging info) which depends on byte offsets we've invalidated by
+    // removing imports or on function indices that we didn't patch up. To be
+    // conservative, we strip all such sections except those we know are okay.
+    // At the moment, we retain producers and target_features sections, as well
+    // as name sections (which Reencode does patch up).
+    fn parse_custom_section(
+        &mut self,
+        module: &mut wasm_encoder::Module,
+        section: wasmparser::CustomSectionReader<'_>,
+    ) -> Result<(), wasm_encoder::reencode::Error<Self::Error>> {
+        match section.as_known() {
+            KnownCustom::Name(_) | KnownCustom::Producers(_) => {
+                // Keep this section verbatim:
+                parse_custom_section(self, module, section)
+            }
+            KnownCustom::Unknown if section.name() == "target_features" => {
+                parse_custom_section(self, module, section)
+            }
+            // Strip others:
+            _ => Ok(()),
+        }
+    }
+}
+
+/// Given a core wasm module that may contain duplicate function imports
+/// (repeats of module/name pairs), returns an equivalent one without the
+/// duplicates, rewriting references to those functions elsewhere in the module
+/// to compensate.
+pub fn dedupe_imports(module: &[u8]) -> Result<Cow<[u8]>, Error> {
+    let mut new_module = wasm_encoder::Module::new();
+    let result =
+        DedupingReencoder::default().parse_core_module(&mut new_module, Parser::new(0), module);
+    match result {
+        // Fast path: return module verbatim
+        Err(DeduperError::UserError(NoDuplicatesFound)) => Ok(Cow::Borrowed(module)),
+        // Rewrite module
+        Ok(_) => Ok(Cow::Owned(new_module.finish())),
+        Err(err) => Err(anyhow!("reencoding failed: {}", err)),
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use wasmparser::{GlobalType, ValType};
+
+    #[test]
+    fn remappings_empty_state() -> Result<(), BinaryReaderError> {
+        let mut remappings = Remappings::default();
+        assert!(remappings.is_empty());
+        assert!(
+            !remappings.is_duplicate(0),
+            "func idxs should default to not being duplicates."
+        );
+        assert_eq!(
+            remappings.new_index_for(7),
+            7,
+            "new_index_for() should return unchanged indices unless it knows a better mapping."
+        );
+        let imports = vec![func("A", "a"), func("B", "b"), func("C", "c")];
+        remappings.imports(imports)?;
+        assert!(
+            remappings.is_empty(),
+            "remappings should show as empty when there are function imports but no duplicates."
+        );
+        Ok(())
+    }
+
+    /// Convenience to construct function imports for test data
+    fn func<'a>(module: &'a str, name: &'a str) -> Result<Import<'a>, BinaryReaderError> {
+        Ok(Import {
+            module,
+            name,
+            ty: TypeRef::Func(0), // We don't care about this.
+        })
+    }
+
+    #[test]
+    fn remappings_populated_state() -> Result<(), BinaryReaderError> {
+        // We test contiguous and discontiguous duplicates, singletons, and
+        // indices outside the mapping table (which represent local, unimported
+        // functions).
+        let imports = vec![
+            func("A", "a"),
+            func("A", "a"),
+            // Include a non-function import to make sure those are skipped and
+            // don't take up a slot in our newly constructed func index space.
+            Ok(Import {
+                module: "non",
+                name: "function",
+                ty: TypeRef::Global(GlobalType {
+                    content_type: ValType::I32,
+                    mutable: false,
+                    shared: false,
+                }),
+            }),
+            func("C", "c"),
+            func("D", "d"),
+            func("D", "d"),
+            func("D", "d"),
+            func("G", "g"),
+            func("D", "d"),
+            func("I", "i"),
+        ];
+        let mut remappings = Remappings::default();
+        remappings.imports(imports)?;
+        assert!(!remappings.is_empty());
+        assert!(!remappings.is_duplicate(0));
+        assert!(remappings.is_duplicate(1));
+        assert_eq!(
+            (0..=9) // Go one beyond the length of the input.
+                .map(|idx| remappings.new_index_for(idx))
+                .collect::<Vec<_>>(),
+            vec![0, 0, 1, 2, 2, 2, 3, 2, 4, 5]
+        );
+        Ok(())
+    }
+
+    /// Show that non-function imports don't throw off the `is_duplicate()`
+    /// positions of later imports.
+    #[test]
+    fn remappings_non_function_imports() -> Result<(), BinaryReaderError> {
+        let imports = vec![
+            func("A", "a"),
+            Ok(Import {
+                module: "non",
+                name: "function",
+                ty: TypeRef::Global(GlobalType {
+                    content_type: ValType::I32,
+                    mutable: false,
+                    shared: false,
+                }),
+            }),
+            func("A", "a"),
+        ];
+        let mut remappings = Remappings::default();
+        remappings.imports(imports)?;
+        assert!(!remappings.is_duplicate(1));
+        assert!(remappings.is_duplicate(2));
+        Ok(())
+    }
+}
diff --git a/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs
index 5f8aa6c9..5c23108b 100644
--- a/crates/wit-parser/src/lib.rs
+++ b/crates/wit-parser/src/lib.rs
@@ -1243,6 +1243,10 @@ impl Stability {
     pub fn is_unknown(&self) -> bool {
         matches!(self, Stability::Unknown)
     }
+
+    pub fn is_stable(&self) -> bool {
+        matches!(self, Stability::Stable { .. })
+    }
 }
 
 impl Default for Stability {
diff --git a/crates/wit-parser/src/resolve.rs b/crates/wit-parser/src/resolve.rs
index a4ff8a15..b47dfba3 100644
--- a/crates/wit-parser/src/resolve.rs
+++ b/crates/wit-parser/src/resolve.rs
@@ -3448,6 +3448,7 @@ impl Remap {
         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();
+        let is_external_include = world.package != include_world.package;
 
         // remove all imports and exports that match the names we're including
         for import in include_world.imports.iter() {
@@ -3465,11 +3466,25 @@ impl Remap {
 
         // copy the imports and exports from the included world into the current world
         for import in include_world.imports.iter() {
-            self.resolve_include_item(names, &mut world.imports, import, span, "import")?;
+            self.resolve_include_item(
+                names,
+                &mut world.imports,
+                import,
+                span,
+                "import",
+                is_external_include,
+            )?;
         }
 
         for export in include_world.exports.iter() {
-            self.resolve_include_item(names, &mut world.exports, export, span, "export")?;
+            self.resolve_include_item(
+                names,
+                &mut world.exports,
+                export,
+                span,
+                "export",
+                is_external_include,
+            )?;
         }
         Ok(())
     }
@@ -3481,6 +3496,7 @@ impl Remap {
         item: (&WorldKey, &WorldItem),
         span: Span,
         item_type: &str,
+        is_external_include: bool,
     ) -> Result<()> {
         match item.0 {
             WorldKey::Name(n) => {
@@ -3515,7 +3531,7 @@ impl Remap {
                         },
                     ) => {
                         assert_eq!(*aid, *bid);
-                        merge_stability(astability, bstability)?;
+                        merge_include_stability(astability, bstability, is_external_include)?;
                     }
                     (WorldItem::Interface { .. }, _) => unreachable!(),
                     (WorldItem::Function(_), _) => unreachable!(),
@@ -3948,25 +3964,18 @@ fn update_stability(from: &Stability, into: &mut Stability) -> Result<()> {
     bail!("mismatch in stability from '{:?}' to '{:?}'", from, into)
 }
 
-/// Compares the two attributes and if the `from` is more stable than the `into` then
-/// it will elevate the `into` to the same stability.
-/// This should be used after its already been confirmed that the types are the same and
-/// should be apart of the component because versions/features are enabled.
-fn merge_stability(from: &Stability, into: &mut Stability) -> Result<()> {
-    // If the two stability annotations are equal then
-    // there's nothing to do here.
-    if from == into {
-        return Ok(());
-    }
-
-    // if the from is more stable elevate stability of into
-    if from > into {
-        *into = from.clone();
+fn merge_include_stability(
+    from: &Stability,
+    into: &mut Stability,
+    is_external_include: bool,
+) -> Result<()> {
+    if is_external_include && from.is_stable() {
+        log::trace!("dropped stability from external package");
+        *into = Stability::Unknown;
         return Ok(());
     }
 
-    // otherwise `into`` already has higher stability
-    return Ok(());
+    return update_stability(from, into);
 }
 
 /// An error that can be returned during "world elaboration" during various
diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs
index 4c9e8310..9725476a 100644
--- a/src/bin/wasm-tools/component.rs
+++ b/src/bin/wasm-tools/component.rs
@@ -160,6 +160,15 @@ pub struct NewOpts {
     /// removal of the old scheme in the future.
     #[clap(long)]
     reject_legacy_names: bool,
+
+    /// Strip out duplicate function imports, which are illegal in components.
+    ///
+    /// Because this rewrites the top of the module and thus changes offsets, it
+    /// conservatively strips any custom sections not known to be immune to this
+    /// rewriting. At present, it retains the producers section, name section,
+    /// and target features section.
+    #[clap(long)]
+    deduplicate_imports: bool,
 }
 
 impl NewOpts {
@@ -172,7 +181,8 @@ impl NewOpts {
         let wasm = self.io.get_input_wasm()?;
         let mut encoder = ComponentEncoder::default()
             .validate(!self.skip_validation)
-            .reject_legacy_names(self.reject_legacy_names);
+            .reject_legacy_names(self.reject_legacy_names)
+            .deduplicate_imports(self.deduplicate_imports);
 
         if let Some(merge) = self.merge_imports_based_on_semver {
             encoder = encoder.merge_imports_based_on_semver(merge);


Test patch:
diff --git a/crates/wit-component/tests/components.rs b/crates/wit-component/tests/components.rs
index 4cb35508..256cbc11 100644
--- a/crates/wit-component/tests/components.rs
+++ b/crates/wit-component/tests/components.rs
@@ -19,7 +19,9 @@ use wit_parser::{PackageId, Resolve, UnresolvedPackageGroup};
 ///   encoded as a component.  If one or more `lib-$name.wat` and/or
 ///   `dlopen-lib-$name.wat` files exist, they will be linked using `Linker`
 ///   such that the `lib-` ones are not `dlopen`-able but the `dlopen-lib-` ones
-///   are.
+///   are. If the first line of `module.wat` is `;; options:
+///   deduplicate-imports`, the equivalent of wasm-tools' `component new
+///   --deduplicate-imports` option will be applied.
 /// * [required] `module.wit` *or* `lib-$name.wat` and `dlopen-lib-$name.wat`
 ///   corresponding to the WAT files above - WIT package(s) describing the
 ///   interfaces of the `module.wat` or `lib-$name.wat` and
@@ -87,11 +89,18 @@ fn run_test(path: &Path) -> Result<()> {
     let module_path = path.join("module.wat");
     let mut adapters = glob::glob(path.join("adapt-*.wat").to_str().unwrap())?;
     let result = if module_path.is_file() {
+        let should_deduplicate = fs::read_to_string(&module_path)?
+            .lines()
+            .next()
+            .unwrap_or("")
+            == ";; options: deduplicate-imports";
         let module = read_core_module(&module_path, &resolve, pkg_id)
             .with_context(|| format!("failed to read core module at {module_path:?}"))?;
         adapters
             .try_fold(
-                ComponentEncoder::default().module(&module)?,
+                ComponentEncoder::default()
+                    .deduplicate_imports(should_deduplicate)
+                    .module(&module)?,
                 |encoder, path| {
                     let (name, wasm) = read_name_and_module("adapt-", &path?, &resolve, pkg_id)?;
                     Ok::<_, Error>(encoder.adapter(&name, &wasm)?)
diff --git a/crates/wit-component/tests/components/deduplicate-imports/adapt-wasi-snapshot-preview1.wat b/crates/wit-component/tests/components/deduplicate-imports/adapt-wasi-snapshot-preview1.wat
new file mode 100644
index 00000000..5fff245e
--- /dev/null
+++ b/crates/wit-component/tests/components/deduplicate-imports/adapt-wasi-snapshot-preview1.wat
@@ -0,0 +1,10 @@
+;; this is a polyfill module that translates from wasi-preview1 to a different
+;; interface
+
+(module
+  (import "foo:foo/my-wasi" "proc-exit" (func $proc_exit (param i32)))
+  (func (export "proc_exit") (param i32)
+    local.get 0
+    call $proc_exit
+  )
+)
diff --git a/crates/wit-component/tests/components/deduplicate-imports/adapt-wasi-snapshot-preview1.wit b/crates/wit-component/tests/components/deduplicate-imports/adapt-wasi-snapshot-preview1.wit
new file mode 100644
index 00000000..d8bd7211
--- /dev/null
+++ b/crates/wit-component/tests/components/deduplicate-imports/adapt-wasi-snapshot-preview1.wit
@@ -0,0 +1,10 @@
+/// This is the interface imported by the `adapt-*.wat` file which is used
+/// to implement the `wasi_snapshot_preview1` interface.
+
+interface my-wasi {
+  proc-exit: func(code: u32);
+}
+
+world adapt-wasi-snapshot-preview1 {
+  import my-wasi;
+}
diff --git a/crates/wit-component/tests/components/deduplicate-imports/component.wat b/crates/wit-component/tests/components/deduplicate-imports/component.wat
new file mode 100644
index 00000000..4987e28a
--- /dev/null
+++ b/crates/wit-component/tests/components/deduplicate-imports/component.wat
@@ -0,0 +1,90 @@
+(component
+  (type (;0;)
+    (instance
+      (type (;0;) (func (param "code" u32)))
+      (export (;0;) "proc-exit" (func (type 0)))
+    )
+  )
+  (import "foo:foo/my-wasi" (instance (;0;) (type 0)))
+  (core module (;0;)
+    (type (;0;) (func (param i32)))
+    (type (;1;) (func))
+    (import "wasi-snapshot-preview1" "proc_exit" (func $j (;0;) (type 0)))
+    (memory (;0;) 1)
+    (export "memory" (memory 0))
+    (func (;1;) (type 1)
+      i32.const 42
+      call $j
+      i32.const 42
+      call $j
+    )
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+      (processed-by "my-fake-bindgen" "123.45")
+    )
+  )
+  (core module (;1;)
+    (type (;0;) (func (param i32)))
+    (import "foo:foo/my-wasi" "proc-exit" (func $proc_exit (;0;) (type 0)))
+    (export "proc_exit" (func 1))
+    (func (;1;) (type 0) (param i32)
+      local.get 0
+      call $proc_exit
+    )
+  )
+  (core module (;2;)
+    (type (;0;) (func (param i32)))
+    (table (;0;) 1 1 funcref)
+    (export "0" (func $adapt-wasi-snapshot-preview1-proc_exit))
+    (export "$imports" (table 0))
+    (func $adapt-wasi-snapshot-preview1-proc_exit (;0;) (type 0) (param i32)
+      local.get 0
+      i32.const 0
+      call_indirect (type 0)
+    )
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core module (;3;)
+    (type (;0;) (func (param i32)))
+    (import "" "0" (func (;0;) (type 0)))
+    (import "" "$imports" (table (;0;) 1 1 funcref))
+    (elem (;0;) (i32.const 0) func 0)
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core instance (;0;) (instantiate 2))
+  (alias core export 0 "0" (core func (;0;)))
+  (core instance (;1;)
+    (export "proc_exit" (func 0))
+  )
+  (core instance (;2;) (instantiate 0
+      (with "wasi-snapshot-preview1" (instance 1))
+    )
+  )
+  (alias core export 2 "memory" (core memory (;0;)))
+  (alias export 0 "proc-exit" (func (;0;)))
+  (core func (;1;) (canon lower (func 0)))
+  (core instance (;3;)
+    (export "proc-exit" (func 1))
+  )
+  (core instance (;4;) (instantiate 1
+      (with "foo:foo/my-wasi" (instance 3))
+    )
+  )
+  (alias core export 0 "$imports" (core table (;0;)))
+  (alias core export 4 "proc_exit" (core func (;2;)))
+  (core instance (;5;)
+    (export "$imports" (table 0))
+    (export "0" (func 2))
+  )
+  (core instance (;6;) (instantiate 3
+      (with "" (instance 5))
+    )
+  )
+  (@producers
+    (processed-by "wit-component" "$CARGO_PKG_VERSION")
+  )
+)
diff --git a/crates/wit-component/tests/components/deduplicate-imports/component.wit.print b/crates/wit-component/tests/components/deduplicate-imports/component.wit.print
new file mode 100644
index 00000000..1aac8daa
--- /dev/null
+++ b/crates/wit-component/tests/components/deduplicate-imports/component.wit.print
@@ -0,0 +1,5 @@
+package root:component;
+
+world root {
+  import foo:foo/my-wasi;
+}
diff --git a/crates/wit-component/tests/components/deduplicate-imports/module.wat b/crates/wit-component/tests/components/deduplicate-imports/module.wat
new file mode 100644
index 00000000..f24541ce
--- /dev/null
+++ b/crates/wit-component/tests/components/deduplicate-imports/module.wat
@@ -0,0 +1,17 @@
+;; options: deduplicate-imports
+(module
+  ;; Import a wasi function twice.
+  (func $i (import "wasi-snapshot-preview1" "proc_exit") (param i32))
+  (func $j (import "wasi-snapshot-preview1" "proc_exit") (param i32))
+
+  ;; Call both copies of the import. They should both end up pointed to the same one.
+  (func
+    i32.const 42
+    call $i
+    i32.const 42
+    call $j
+  )
+
+  ;; Required by wasi
+  (memory (export "memory") 1)
+)
diff --git a/crates/wit-component/tests/components/deduplicate-imports/module.wit b/crates/wit-component/tests/components/deduplicate-imports/module.wit
new file mode 100644
index 00000000..f5d68f90
--- /dev/null
+++ b/crates/wit-component/tests/components/deduplicate-imports/module.wit
@@ -0,0 +1,2 @@
+package foo:foo;
+world module {}
diff --git a/crates/wit-parser/tests/ui/gated-include.wit b/crates/wit-parser/tests/ui/gated-include.wit
new file mode 100644
index 00000000..74df3945
--- /dev/null
+++ b/crates/wit-parser/tests/ui/gated-include.wit
@@ -0,0 +1,67 @@
+package wasmtime:test@0.1.0;
+
+interface unknown-stability-interface {
+    resource unknown-stability-resource {
+    }
+    stable-func: func();
+}
+
+@unstable(feature = active)
+interface unstable-interface {
+    @unstable(feature = active)
+    resource unstable-resource {
+    }
+    @unstable(feature = active)
+    unstable-func: func();
+}
+
+@since(version = 0.1.0)
+interface stable-interface {
+    @since(version = 0.1.0)
+    resource stable-resource {
+    }
+    @since(version = 0.1.0)
+    stable-func: func();
+}
+
+world unknown-stability {
+    import unknown-stability-interface;
+}
+
+world unstable {
+    @unstable(feature = active)
+    import unstable-interface;
+}
+
+world stable{
+    @since(version = 0.1.0)
+    import stable-interface;
+}
+
+world simple-include {
+    include unstable;
+    include stable;
+    include unknown-stability;
+}
+
+world unstable-include-in-package 
+{
+    include unstable;
+}
+
+world dup-include-in-package {
+    include simple-include;
+    include unstable-include-in-package;
+}
+
+world dup-use-package {
+    @unstable(feature = active)
+    use stable-interface.{stable-resource};
+    include simple-include;
+}
+
+world dup-use-package-ordered {
+    include simple-include;
+    @unstable(feature = active)
+    use stable-interface.{stable-resource};
+}
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/gated-include.wit.json b/crates/wit-parser/tests/ui/gated-include.wit.json
new file mode 100644
index 00000000..5f00420f
--- /dev/null
+++ b/crates/wit-parser/tests/ui/gated-include.wit.json
@@ -0,0 +1,346 @@
+{
+  "worlds": [
+    {
+      "name": "unknown-stability",
+      "imports": {
+        "interface-0": {
+          "interface": {
+            "id": 0
+          }
+        }
+      },
+      "exports": {},
+      "package": 0
+    },
+    {
+      "name": "unstable",
+      "imports": {
+        "interface-1": {
+          "interface": {
+            "id": 1,
+            "stability": {
+              "unstable": {
+                "feature": "active"
+              }
+            }
+          }
+        }
+      },
+      "exports": {},
+      "package": 0
+    },
+    {
+      "name": "stable",
+      "imports": {
+        "interface-2": {
+          "interface": {
+            "id": 2,
+            "stability": {
+              "stable": {
+                "since": "0.1.0"
+              }
+            }
+          }
+        }
+      },
+      "exports": {},
+      "package": 0
+    },
+    {
+      "name": "simple-include",
+      "imports": {
+        "interface-1": {
+          "interface": {
+            "id": 1,
+            "stability": {
+              "unstable": {
+                "feature": "active"
+              }
+            }
+          }
+        },
+        "interface-2": {
+          "interface": {
+            "id": 2,
+            "stability": {
+              "stable": {
+                "since": "0.1.0"
+              }
+            }
+          }
+        },
+        "interface-0": {
+          "interface": {
+            "id": 0
+          }
+        }
+      },
+      "exports": {},
+      "package": 0
+    },
+    {
+      "name": "unstable-include-in-package",
+      "imports": {
+        "interface-1": {
+          "interface": {
+            "id": 1,
+            "stability": {
+              "unstable": {
+                "feature": "active"
+              }
+            }
+          }
+        }
+      },
+      "exports": {},
+      "package": 0
+    },
+    {
+      "name": "dup-include-in-package",
+      "imports": {
+        "interface-1": {
+          "interface": {
+            "id": 1,
+            "stability": {
+              "unstable": {
+                "feature": "active"
+              }
+            }
+          }
+        },
+        "interface-2": {
+          "interface": {
+            "id": 2,
+            "stability": {
+              "stable": {
+                "since": "0.1.0"
+              }
+            }
+          }
+        },
+        "interface-0": {
+          "interface": {
+            "id": 0
+          }
+        }
+      },
+      "exports": {},
+      "package": 0
+    },
+    {
+      "name": "dup-use-package",
+      "imports": {
+        "interface-1": {
+          "interface": {
+            "id": 1,
+            "stability": {
+              "unstable": {
+                "feature": "active"
+              }
+            }
+          }
+        },
+        "interface-2": {
+          "interface": {
+            "id": 2,
+            "stability": {
+              "stable": {
+                "since": "0.1.0"
+              }
+            }
+          }
+        },
+        "interface-0": {
+          "interface": {
+            "id": 0
+          }
+        },
+        "stable-resource": {
+          "type": 3
+        }
+      },
+      "exports": {},
+      "package": 0
+    },
+    {
+      "name": "dup-use-package-ordered",
+      "imports": {
+        "interface-1": {
+          "interface": {
+            "id": 1,
+            "stability": {
+              "unstable": {
+                "feature": "active"
+              }
+            }
+          }
+        },
+        "interface-2": {
+          "interface": {
+            "id": 2,
+            "stability": {
+              "stable": {
+                "since": "0.1.0"
+              }
+            }
+          }
+        },
+        "interface-0": {
+          "interface": {
+            "id": 0
+          }
+        },
+        "stable-resource": {
+          "type": 4
+        }
+      },
+      "exports": {},
+      "package": 0
+    }
+  ],
+  "interfaces": [
+    {
+      "name": "unknown-stability-interface",
+      "types": {
+        "unknown-stability-resource": 0
+      },
+      "functions": {
+        "stable-func": {
+          "name": "stable-func",
+          "kind": "freestanding",
+          "params": []
+        }
+      },
+      "package": 0
+    },
+    {
+      "name": "unstable-interface",
+      "types": {
+        "unstable-resource": 1
+      },
+      "functions": {
+        "unstable-func": {
+          "name": "unstable-func",
+          "kind": "freestanding",
+          "params": [],
+          "stability": {
+            "unstable": {
+              "feature": "active"
+            }
+          }
+        }
+      },
+      "stability": {
+        "unstable": {
+          "feature": "active"
+        }
+      },
+      "package": 0
+    },
+    {
+      "name": "stable-interface",
+      "types": {
+        "stable-resource": 2
+      },
+      "functions": {
+        "stable-func": {
+          "name": "stable-func",
+          "kind": "freestanding",
+          "params": [],
+          "stability": {
+            "stable": {
+              "since": "0.1.0"
+            }
+          }
+        }
+      },
+      "stability": {
+        "stable": {
+          "since": "0.1.0"
+        }
+      },
+      "package": 0
+    }
+  ],
+  "types": [
+    {
+      "name": "unknown-stability-resource",
+      "kind": "resource",
+      "owner": {
+        "interface": 0
+      }
+    },
+    {
+      "name": "unstable-resource",
+      "kind": "resource",
+      "owner": {
+        "interface": 1
+      },
+      "stability": {
+        "unstable": {
+          "feature": "active"
+        }
+      }
+    },
+    {
+      "name": "stable-resource",
+      "kind": "resource",
+      "owner": {
+        "interface": 2
+      },
+      "stability": {
+        "stable": {
+          "since": "0.1.0"
+        }
+      }
+    },
+    {
+      "name": "stable-resource",
+      "kind": {
+        "type": 2
+      },
+      "owner": {
+        "world": 6
+      },
+      "stability": {
+        "unstable": {
+          "feature": "active"
+        }
+      }
+    },
+    {
+      "name": "stable-resource",
+      "kind": {
+        "type": 2
+      },
+      "owner": {
+        "world": 7
+      },
+      "stability": {
+        "unstable": {
+          "feature": "active"
+        }
+      }
+    }
+  ],
+  "packages": [
+    {
+      "name": "wasmtime:test@0.1.0",
+      "interfaces": {
+        "unknown-stability-interface": 0,
+        "unstable-interface": 1,
+        "stable-interface": 2
+      },
+      "worlds": {
+        "unknown-stability": 0,
+        "unstable": 1,
+        "stable": 2,
+        "simple-include": 3,
+        "unstable-include-in-package": 4,
+        "dup-include-in-package": 5,
+        "dup-use-package": 6,
+        "dup-use-package-ordered": 7
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/gated-include-use-with-stable.wit b/crates/wit-parser/tests/ui/multi-package-gated-include.wit
similarity index 63%
rename from crates/wit-parser/tests/ui/gated-include-use-with-stable.wit
rename to crates/wit-parser/tests/ui/multi-package-gated-include.wit
index 159ffb39..2dd3ff41 100644
--- a/crates/wit-parser/tests/ui/gated-include-use-with-stable.wit
+++ b/crates/wit-parser/tests/ui/multi-package-gated-include.wit
@@ -4,13 +4,36 @@ world test{
     @unstable(feature = active)
     include wasi:unstable/imports@0.2.3; 
     include wasi:foo/imports@0.2.3;
-} 
+    include wasi:someother/imports@0.2.3;
+}
 
-world test-ordered{
+world test-ordered {
+    include wasi:someother/imports@0.2.3;
     include wasi:foo/imports@0.2.3;
     @unstable(feature = active)
     include wasi:unstable/imports@0.2.3; 
-} 
+}
+
+world test-no-stability {
+    include wasi:someother/imports@0.2.3;
+    include wasi:foo/imports@0.2.3;
+    include wasi:unstable/imports@0.2.3; 
+}
+
+world test-only-stable {
+    include wasi:foo/imports@0.2.3;
+}
+
+world test-only-stable-with-feature {
+    @unstable(feature = active)
+    include wasi:foo/imports@0.2.3;
+}
+
+// in test this results in world with nothing
+world test-only-stable-with-in-active-feature {
+    @unstable(feature = in-active)
+    include wasi:foo/imports@0.2.3;
+}
 
 package wasi:unstable@0.2.3 {
     @unstable(feature = active)
@@ -24,6 +47,22 @@ package wasi:unstable@0.2.3 {
     }
 }
 
+package wasi:someother@0.2.3 {
+    @since(version = 0.2.0)
+    world imports {
+        import someother;
+    }
+
+    @since(version = 0.2.0)
+    interface someother {
+        @since(version = 0.2.0)
+        resource someother-resource {
+        }
+    }
+}
+
+
+
 package wasi:foo@0.2.3 {
     @since(version = 0.2.0)
     world imports {
@@ -40,7 +79,7 @@ package wasi:dep2@0.2.3 {
         @since(version = 0.2.0)
         import stable;
     }
-    @since(version = 0.2.1)
+    @since(version = 0.2.0)
     interface stable {
         resource stable-resource {
         }
diff --git a/crates/wit-parser/tests/ui/gated-include-use-with-stable.wit.json b/crates/wit-parser/tests/ui/multi-package-gated-include.wit.json
similarity index 64%
rename from crates/wit-parser/tests/ui/gated-include-use-with-stable.wit.json
rename to crates/wit-parser/tests/ui/multi-package-gated-include.wit.json
index b7f857b1..fc917185 100644
--- a/crates/wit-parser/tests/ui/gated-include-use-with-stable.wit.json
+++ b/crates/wit-parser/tests/ui/multi-package-gated-include.wit.json
@@ -61,12 +61,7 @@
       "imports": {
         "interface-2": {
           "interface": {
-            "id": 2,
-            "stability": {
-              "stable": {
-                "since": "0.2.0"
-              }
-            }
+            "id": 2
           }
         },
         "interface-1": {
@@ -93,6 +88,23 @@
         }
       }
     },
+    {
+      "name": "imports",
+      "imports": {
+        "interface-3": {
+          "interface": {
+            "id": 3
+          }
+        }
+      },
+      "exports": {},
+      "package": 4,
+      "stability": {
+        "stable": {
+          "since": "0.2.0"
+        }
+      }
+    },
     {
       "name": "imports",
       "imports": {
@@ -127,17 +139,17 @@
           }
         },
         "stable-resource": {
-          "type": 3
+          "type": 4
         },
         "unversioned-resource": {
-          "type": 4
+          "type": 5
         },
         "unstable-resource": {
-          "type": 5
+          "type": 6
         }
       },
       "exports": {},
-      "package": 4,
+      "package": 5,
       "stability": {
         "unstable": {
           "feature": "active"
@@ -151,8 +163,8 @@
           "interface": {
             "id": 2,
             "stability": {
-              "stable": {
-                "since": "0.2.0"
+              "unstable": {
+                "feature": "active"
               }
             }
           }
@@ -177,28 +189,38 @@
             }
           }
         },
+        "interface-3": {
+          "interface": {
+            "id": 3
+          }
+        },
         "stable-resource": {
-          "type": 3
+          "type": 4
         },
         "unversioned-resource": {
-          "type": 4
+          "type": 5
         },
         "unstable-resource": {
-          "type": 5
+          "type": 6
         }
       },
       "exports": {},
-      "package": 5
+      "package": 6
     },
     {
       "name": "test-ordered",
       "imports": {
+        "interface-3": {
+          "interface": {
+            "id": 3
+          }
+        },
         "interface-2": {
           "interface": {
             "id": 2,
             "stability": {
-              "stable": {
-                "since": "0.2.0"
+              "unstable": {
+                "feature": "active"
               }
             }
           }
@@ -224,17 +246,131 @@
           }
         },
         "stable-resource": {
-          "type": 3
+          "type": 4
         },
         "unversioned-resource": {
-          "type": 4
+          "type": 5
         },
         "unstable-resource": {
+          "type": 6
+        }
+      },
+      "exports": {},
+      "package": 6
+    },
+    {
+      "name": "test-no-stability",
+      "imports": {
+        "interface-3": {
+          "interface": {
+            "id": 3
+          }
+        },
+        "interface-2": {
+          "interface": {
+            "id": 2,
+            "stability": {
+              "unstable": {
+                "feature": "active"
+              }
+            }
+          }
+        },
+        "interface-1": {
+          "interface": {
+            "id": 1,
+            "stability": {
+              "unstable": {
+                "feature": "active"
+              }
+            }
+          }
+        },
+        "interface-0": {
+          "interface": {
+            "id": 0,
+            "stability": {
+              "unstable": {
+                "feature": "active"
+              }
+            }
+          }
+        },
+        "stable-resource": {
+          "type": 4
+        },
+        "unversioned-resource": {
           "type": 5
+        },
+        "unstable-resource": {
+          "type": 6
+        }
+      },
+      "exports": {},
+      "package": 6
+    },
+    {
+      "name": "test-only-stable",
+      "imports": {
+        "interface-2": {
+          "interface": {
+            "id": 2
+          }
+        },
+        "interface-1": {
+          "interface": {
+            "id": 1
+          }
+        },
+        "interface-0": {
+          "interface": {
+            "id": 0,
+            "stability": {
+              "unstable": {
+                "feature": "active"
+              }
+            }
+          }
+        }
+      },
+      "exports": {},
+      "package": 6
+    },
+    {
+      "name": "test-only-stable-with-feature",
+      "imports": {
+        "interface-2": {
+          "interface": {
+            "id": 2
+          }
+        },
+        "interface-1": {
+          "interface": {
+            "id": 1
+          }
+        },
+        "interface-0": {
+          "interface": {
+            "id": 0,
+            "stability": {
+              "unstable": {
+                "feature": "active"
+              }
+            }
+          }
         }
       },
       "exports": {},
-      "package": 5
+      "package": 6
+    },
+    {
+      "name": "test-only-stable-with-in-active-feature",
+      "imports": {},
+      "exports": {},
+      "package": 6,
+      "docs": {
+        "contents": "in test this results in world with nothing"
+      }
     }
   ],
   "interfaces": [
@@ -267,10 +403,23 @@
       "functions": {},
       "stability": {
         "stable": {
-          "since": "0.2.1"
+          "since": "0.2.0"
         }
       },
       "package": 2
+    },
+    {
+      "name": "someother",
+      "types": {
+        "someother-resource": 3
+      },
+      "functions": {},
+      "stability": {
+        "stable": {
+          "since": "0.2.0"
+        }
+      },
+      "package": 4
     }
   ],
   "types": [
@@ -300,13 +449,25 @@
         "interface": 2
       }
     },
+    {
+      "name": "someother-resource",
+      "kind": "resource",
+      "owner": {
+        "interface": 3
+      },
+      "stability": {
+        "stable": {
+          "since": "0.2.0"
+        }
+      }
+    },
     {
       "name": "stable-resource",
       "kind": {
         "type": 2
       },
       "owner": {
-        "world": 4
+        "world": 5
       },
       "stability": {
         "unstable": {
@@ -320,7 +481,7 @@
         "type": 1
       },
       "owner": {
-        "world": 4
+        "world": 5
       },
       "stability": {
         "unstable": {
@@ -334,7 +495,7 @@
         "type": 0
       },
       "owner": {
-        "world": 4
+        "world": 5
       },
       "stability": {
         "unstable": {
@@ -378,19 +539,32 @@
         "imports": 3
       }
     },
+    {
+      "name": "wasi:someother@0.2.3",
+      "interfaces": {
+        "someother": 3
+      },
+      "worlds": {
+        "imports": 4
+      }
+    },
     {
       "name": "wasi:unstable@0.2.3",
       "interfaces": {},
       "worlds": {
-        "imports": 4
+        "imports": 5
       }
     },
     {
       "name": "wasmtime:test",
       "interfaces": {},
       "worlds": {
-        "test": 5,
-        "test-ordered": 6
+        "test": 6,
+        "test-ordered": 7,
+        "test-no-stability": 8,
+        "test-only-stable": 9,
+        "test-only-stable-with-feature": 10,
+        "test-only-stable-with-in-active-feature": 11
       }
     }
   ]