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:
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
       }
     }
   ]