Repository: bytecodealliance/wasm-tools
Pull request #1261 (2023-10-24 19:13:09)

implement resource support for `wasm-compose`
This aims to support the specific case of passing handles to imported resource types across component boundaries.  ~~It does not include any support for guest-exported resources (as would be required for most virtualization use cases).~~ Update: guest-exported resources can now be connected to imports, and I've updated `wasi-virt` to use that, with all tests passing.

I've updated the middleware example to use resources to demonstrate the concept. To make it more realistic, we could consider using real `wasi-http` request and response resources.

TODO items:

~~- Add tests~~

~~- Add missing docs~~

dicej: FYI, we ran into some bugs with a more complex example involving `wasi-http`.  I'll push updates once I've got them straightened out.
morenol: Thanks for this PR!
dicej: FYI, I won't have much time to work on this PR in the next few weeks since I've got other things to focus on.  Hoping to come back to it starting mid November.  Which is to say: feel free to jump in and help if this is blocking you :)
dicej: > Regarding guest-exported resources, what's the plan to get that implemented for the wasi-virt use case?

Sorry, just noticed your question.  I don't have an immediate plan for that.  I agree it's a super important case, but not as urgent for me as guest-imported resources at the moment.  I'd love to help out with it when I'm a bit less busy.
dicej: FYI, I've started to work on resource exports and virtualization-style composition today.  Will push updates as I make progress.
dicej: I've added docs and tests, squashed, and rebased.  Here's squashed commit message, for reference:

> The basic approach is as follows:
> 
> - Start with an empty resource remapping (i.e. no given pair of distinct
>   `ResourceId`s are considered equivalent)
> 
> - For instance import and export pair we're considering connecting to each
>   other, match up any resource types by name and optimistically produce a
>   remapping that sets identically-named pairs equal to each other, provided that
>   remapping does not contradict any previous remappings.
> 
> - If the above fails (either because a consistent remapping could not be created
>   or because the instances were incompatible for other reasons), throw away the
>   changes and reject the connection (which may mean giving up on the whole
>   composition if we can't do what the user requested).  Otherwise, store the
>   remapping changes and accept the connection.
> 
> - Once all such connections have been made, update the remapping to set all
>   remaining, unconnected, identically-named resource imports in
>   identically-named instances equal to each other.
> 
> - Finally, encode the output component using the remapping produced above.
> 
> Note that I'm using `RefCell` for interior mutability since the places I needed
> to update the remapping only had a shared reference to the `CompsitionGraph`,
> and refactoring them to provide unique references felt like overkill (and I've
> heard a major refactor of `wasm-compose` is planned anyway, so perhaps this can
> be revisited then).
> 
> I've brought in `im-rc` as a dependency since it makes rolling back tentative
> changes trivial.  There may be more efficient approaches if that's a concern.
> 
> Per Alex's advice, I've changed a few types and traits in `wasmparser::types`
> from `pub(crate)` to `pub` in order to use them in the algorithm described
> above.

dicej: Thanks for the feedback, @peterhuene and @alexcrichton.  I'll work on addressing it soon.

First, though: my colleague found a way to make it panic during encoding, so I'm going debug that.
dicej: > First, though: my colleague found a way to make it panic during encoding, so I'm going debug that.

Turns out this was related to the incomplete state of `wasi-http` virtualization in `wasi-virt` as discussed [here](https://bytecodealliance.zulipchat.com/#narrow/stream/219900-wasi/topic/.60wasi-http.60.20middleware.20is.20infeasible.3F/near/402081439), which resulted in an inconsistent composition graph.  That's something `wasm-compose` should catch while creating the graph rather than panic during encoding.  I'll see if I can reduce that to a manageable test case, but meanwhile I've created #1289.
alexcrichton: Ok I'm going to go ahead and merge this since I think it's in a good state, and it's ok to follow-up with fixes if necessary of course

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index be8a3523..0fa6d597 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -157,6 +157,15 @@ version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
 
+[[package]]
+name = "bitmaps"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2"
+dependencies = [
+ "typenum",
+]
+
 [[package]]
 name = "blake3"
 version = "1.4.1"
@@ -817,6 +826,20 @@ dependencies = [
  "unicode-normalization",
 ]
 
+[[package]]
+name = "im-rc"
+version = "15.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe"
+dependencies = [
+ "bitmaps",
+ "rand_core",
+ "rand_xoshiro",
+ "sized-chunks",
+ "typenum",
+ "version_check",
+]
+
 [[package]]
 name = "indexmap"
 version = "1.9.3"
@@ -1188,6 +1211,15 @@ dependencies = [
  "getrandom",
 ]
 
+[[package]]
+name = "rand_xoshiro"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa"
+dependencies = [
+ "rand_core",
+]
+
 [[package]]
 name = "rayon"
 version = "1.7.0"
@@ -1392,6 +1424,16 @@ dependencies = [
  "unsafe-libyaml",
 ]
 
+[[package]]
+name = "sized-chunks"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e"
+dependencies = [
+ "bitmaps",
+ "typenum",
+]
+
 [[package]]
 name = "slice-group-by"
 version = "0.3.1"
@@ -1712,6 +1754,7 @@ dependencies = [
  "anyhow",
  "glob",
  "heck",
+ "im-rc",
  "indexmap 2.0.0",
  "log",
  "petgraph",
diff --git a/crates/wasm-compose/Cargo.toml b/crates/wasm-compose/Cargo.toml
index 9da877e9..9981af1c 100644
--- a/crates/wasm-compose/Cargo.toml
+++ b/crates/wasm-compose/Cargo.toml
@@ -23,6 +23,7 @@ log = { workspace = true }
 serde_yaml = "0.9.22"
 smallvec = "1.10.0"
 heck = "0.4.0"
+im-rc = "15.1.0"
 
 [dev-dependencies]
 glob = "0.3.0"
diff --git a/crates/wasm-compose/README.md b/crates/wasm-compose/README.md
index 644b1932..b7454f44 100644
--- a/crates/wasm-compose/README.md
+++ b/crates/wasm-compose/README.md
@@ -34,7 +34,7 @@ has implementation efforts underway to support it.__
 
 ## Usage
 
-To composed a component, run the `compose` command:
+To compose a component, run the `compose` command:
 
 ```sh
 wasm-tools compose -o composed.wasm component.wasm
diff --git a/crates/wasm-compose/example/middleware/Cargo.toml b/crates/wasm-compose/example/middleware/Cargo.toml
index 91d426e2..eafcee67 100644
--- a/crates/wasm-compose/example/middleware/Cargo.toml
+++ b/crates/wasm-compose/example/middleware/Cargo.toml
@@ -6,7 +6,7 @@ publish = false
 
 [dependencies]
 flate2 = "1.0.24"
-cargo-component-bindings = { git = "https://github.com/bytecodealliance/cargo-component" }
+cargo-component-bindings = "0.4.0"
 
 [lib]
 crate-type = ["cdylib"]
diff --git a/crates/wasm-compose/example/middleware/src/lib.rs b/crates/wasm-compose/example/middleware/src/lib.rs
index d1870c51..9aa2ad06 100644
--- a/crates/wasm-compose/example/middleware/src/lib.rs
+++ b/crates/wasm-compose/example/middleware/src/lib.rs
@@ -1,50 +1,43 @@
 cargo_component_bindings::generate!();
 
 use bindings::{
-    example::service::handler as downstream,
-    exports::example::service::handler::{Error, Handler, Request, Response},
+    example::service::{
+        handler as downstream, logging,
+        types::{Error, Request, Response},
+    },
+    exports::example::service::handler,
 };
 use flate2::{write::GzEncoder, Compression};
 use std::io::Write;
 
 struct Component;
 
-impl Handler for Component {
+impl handler::Guest for Component {
     fn execute(req: Request) -> Result<Response, Error> {
+        logging::get_logger().log("entering middleware execute function");
+
         // Send the request to the downstream service
-        let mut response = downstream::execute(&downstream::Request {
-            headers: req.headers,
-            body: req.body,
-        })
-        .map(|r| Response {
-            headers: r.headers,
-            body: r.body,
-        })
-        .map_err(|e| match e {
-            downstream::Error::BadRequest => Error::BadRequest,
-        })?;
+        let response = downstream::execute(Request::new(&req.headers(), &req.body()))?;
+
+        let mut headers = response.headers();
 
         // If the response is already encoded, leave it alone
-        if response
-            .headers
-            .iter()
-            .any(|(k, _)| k == b"content-encoding")
-        {
+        if headers.iter().any(|(k, _)| k == b"content-encoding") {
             return Ok(response);
         }
 
         // Set the `content-encoding` header to `gzip`
-        response
-            .headers
-            .push((b"content-encoding".to_vec(), b"gzip".to_vec()));
+        headers.push((b"content-encoding".to_vec(), b"gzip".to_vec()));
 
         // Compress the response body
         let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
         encoder
-            .write_all(&response.body)
+            .write_all(&response.body())
             .map_err(|_| Error::BadRequest)?;
-        response.body = encoder.finish().map_err(|_| Error::BadRequest)?;
 
-        Ok(response)
+        Ok(Response::new(
+            &headers,
+            &encoder.finish().map_err(|_| Error::BadRequest)?,
+        ))
     }
 }
diff --git a/crates/wasm-compose/example/server/Cargo.toml b/crates/wasm-compose/example/server/Cargo.toml
index 2cce17dd..d73ff219 100644
--- a/crates/wasm-compose/example/server/Cargo.toml
+++ b/crates/wasm-compose/example/server/Cargo.toml
@@ -9,9 +9,10 @@ async-std = { version = "1.12.0", features = ["attributes"] }
 clap = { version = "4.3.19", features = ["derive"] }
 driftwood = "0.0.7"
 tide = "0.16.0"
-wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "7b9189b", features = ["component-model"] }
-wasmtime-wasi = { git = "https://github.com/bytecodealliance/wasmtime", rev = "7b9189b" }
-wasi-cap-std-sync = { git = "https://github.com/bytecodealliance/wasmtime", rev = "7b9189b" }
+wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", branch = "release-14.0.0", features = ["component-model"] }
+wasmtime-wasi = { git = "https://github.com/bytecodealliance/wasmtime", branch = "release-14.0.0" }
+wasi-cap-std-sync = { git = "https://github.com/bytecodealliance/wasmtime", branch = "release-14.0.0" }
 anyhow = "1.0.72"
+async-trait = "0.1.74"
 
 [workspace]
diff --git a/crates/wasm-compose/example/server/config.yml b/crates/wasm-compose/example/server/config.yml
index 36715256..c9261947 100644
--- a/crates/wasm-compose/example/server/config.yml
+++ b/crates/wasm-compose/example/server/config.yml
@@ -4,4 +4,9 @@ search-paths:
 instantiations:
   $input:
     arguments:
-      example:service/handler@0.1.0: svc
+      example:service/handler@0.1.0:
+        instance: svc
+        export: example:service/handler@0.1.0
+      example:service/logging@0.1.0:
+        instance: svc
+        export: example:service/logging@0.1.0
diff --git a/crates/wasm-compose/example/server/src/main.rs b/crates/wasm-compose/example/server/src/main.rs
index 7b3010cb..301ec961 100644
--- a/crates/wasm-compose/example/server/src/main.rs
+++ b/crates/wasm-compose/example/server/src/main.rs
@@ -1,3 +1,4 @@
+use async_trait::async_trait;
 use clap::Parser;
 use std::path::{Path, PathBuf};
 use tide::{
@@ -12,11 +13,15 @@ use tide::{
 use wasmtime::{component::*, Config, Engine, Store};
 use wasmtime_wasi::preview2::{command, Table, WasiCtx, WasiCtxBuilder, WasiView};
 
-use exports::example::service::*;
+use example::service::{
+    logging::{self, HostLogger},
+    types::{self, HostRequest, HostResponse},
+};
+use exports::example::service::handler;
 
 wasmtime::component::bindgen!({
     path: "../service.wit",
-    world: "service",
+    world: "service-for-host",
     async: true
 });
 
@@ -44,23 +49,6 @@ impl State {
     }
 }
 
-impl TryFrom<handler::Response> for tide::Response {
-    type Error = tide::Error;
-
-    fn try_from(r: handler::Response) -> Result<Self, Self::Error> {
-        // Convert the service response to a tide response
-        let mut builder = tide::Response::builder(StatusCode::Ok);
-        for (name, value) in r.headers {
-            builder = builder.header(
-                HeaderName::from_bytes(name)?,
-                HeaderValue::from_bytes(value)?,
-            );
-        }
-        builder = builder.body(r.body);
-        Ok(builder.build())
-    }
-}
-
 impl handler::Error {
     fn into_tide(self) -> tide::Error {
         match self {
@@ -130,17 +118,38 @@ impl ServerApp {
         let state = req.state();
         let mut linker = Linker::new(&state.engine);
         command::add_to_linker(&mut linker)?;
+        ServiceForHost::add_to_linker(&mut linker, |view| view)?;
 
-        let wasi_view = ServerWasiView::new()?;
+        let wasi_view = ServerWasiView::new();
         let mut store = Store::new(&state.engine, wasi_view);
         let (service, _) =
-            Service::instantiate_async(&mut store, &state.component, &linker).await?;
-        service
+            ServiceForHost::instantiate_async(&mut store, &state.component, &linker).await?;
+        let request = Resource::<types::Request>::new_own(
+            store
+                .data_mut()
+                .table_mut()
+                .push(Box::new(MyRequest { headers, body }))?,
+        );
+        let response = service
             .example_service_handler()
-            .call_execute(&mut store, &handler::Request { headers, body })
+            .call_execute(&mut store, request)
             .await?
-            .map(TryInto::try_into)
-            .map_err(handler::Error::into_tide)?
+            .map_err(handler::Error::into_tide)?;
+
+        let response = store
+            .data_mut()
+            .table_mut()
+            .delete::<MyResponse>(response.rep())?;
+
+        let mut builder = tide::Response::builder(StatusCode::Ok);
+        for (name, value) in response.headers {
+            builder = builder.header(
+                HeaderName::from_bytes(name)?,
+                HeaderValue::from_bytes(value)?,
+            );
+        }
+        builder = builder.body(response.body);
+        Ok(builder.build())
     }
 }
 
@@ -150,11 +159,11 @@ struct ServerWasiView {
 }
 
 impl ServerWasiView {
-    fn new() -> Result<Self, anyhow::Error> {
-        let mut table = Table::new();
-        let ctx = WasiCtxBuilder::new().inherit_stdio().build(&mut table)?;
+    fn new() -> Self {
+        let table = Table::new();
+        let ctx = WasiCtxBuilder::new().inherit_stdio().build();
 
-        Ok(Self { table, ctx })
+        Self { table, ctx }
     }
 }
 
@@ -176,6 +185,115 @@ impl WasiView for ServerWasiView {
     }
 }
 
+struct MyRequest {
+    headers: Vec<(Vec<u8>, Vec<u8>)>,
+    body: Vec<u8>,
+}
+
+#[async_trait]
+impl HostRequest for ServerWasiView {
+    async fn new(
+        &mut self,
+        headers: Vec<(Vec<u8>, Vec<u8>)>,
+        body: Vec<u8>,
+    ) -> anyhow::Result<Resource<types::Request>> {
+        Ok(Resource::new_own(
+            self.table_mut()
+                .push(Box::new(MyRequest { headers, body }))?,
+        ))
+    }
+
+    async fn headers(
+        &mut self,
+        this: Resource<types::Request>,
+    ) -> anyhow::Result<Vec<(Vec<u8>, Vec<u8>)>> {
+        Ok(self.table().get::<MyRequest>(this.rep())?.headers.clone())
+    }
+
+    async fn body(&mut self, this: Resource<types::Request>) -> anyhow::Result<Vec<u8>> {
+        Ok(self.table().get::<MyRequest>(this.rep())?.body.clone())
+    }
+
+    fn drop(&mut self, this: Resource<types::Request>) -> anyhow::Result<()> {
+        Ok(self
+            .table_mut()
+            .delete::<MyRequest>(this.rep())
+            .map(|_| ())?)
+    }
+}
+
+struct MyResponse {
+    headers: Vec<(Vec<u8>, Vec<u8>)>,
+    body: Vec<u8>,
+}
+
+#[async_trait]
+impl HostResponse for ServerWasiView {
+    async fn new(
+        &mut self,
+        headers: Vec<(Vec<u8>, Vec<u8>)>,
+        body: Vec<u8>,
+    ) -> anyhow::Result<Resource<types::Response>> {
+        Ok(Resource::new_own(
+            self.table_mut()
+                .push(Box::new(MyResponse { headers, body }))?,
+        ))
+    }
+
+    async fn headers(
+        &mut self,
+        this: Resource<types::Response>,
+    ) -> anyhow::Result<Vec<(Vec<u8>, Vec<u8>)>> {
+        Ok(self.table().get::<MyResponse>(this.rep())?.headers.clone())
+    }
+
+    async fn body(&mut self, this: Resource<types::Response>) -> anyhow::Result<Vec<u8>> {
+        Ok(self.table().get::<MyResponse>(this.rep())?.body.clone())
+    }
+
+    fn drop(&mut self, this: Resource<types::Response>) -> anyhow::Result<()> {
+        Ok(self
+            .table_mut()
+            .delete::<MyResponse>(this.rep())
+            .map(|_| ())?)
+    }
+}
+
+impl types::Host for ServerWasiView {}
+
+struct MyLogger;
+
+#[async_trait]
+impl HostLogger for ServerWasiView {
+    async fn log(
+        &mut self,
+        this: Resource<logging::Logger>,
+        message: String,
+    ) -> anyhow::Result<()> {
+        self.table().get::<MyLogger>(this.rep())?;
+
+        println!("host logger: {message}");
+
+        Ok(())
+    }
+
+    fn drop(&mut self, this: Resource<logging::Logger>) -> anyhow::Result<()> {
+        Ok(self
+            .table_mut()
+            .delete::<MyLogger>(this.rep())
+            .map(|_| ())?)
+    }
+}
+
+#[async_trait]
+impl logging::Host for ServerWasiView {
+    async fn get_logger(&mut self) -> anyhow::Result<Resource<logging::Logger>> {
+        Ok(Resource::new_own(
+            self.table_mut().push(Box::new(MyLogger))?,
+        ))
+    }
+}
+
 #[async_std::main]
 async fn main() -> tide::Result<()> {
     // Parse the command line arguments and run the application
diff --git a/crates/wasm-compose/example/service.wit b/crates/wasm-compose/example/service.wit
index d5888d45..cbb04698 100644
--- a/crates/wasm-compose/example/service.wit
+++ b/crates/wasm-compose/example/service.wit
@@ -1,28 +1,50 @@
 package example:service@0.1.0
 
-interface handler {
-    record request {
-        headers: list<tuple<list<u8>, list<u8>>>,
-        body: list<u8>,
+interface types {
+    resource request {
+        constructor(headers: list<tuple<list<u8>, list<u8>>>, body: list<u8>)
+        headers: func() -> list<tuple<list<u8>, list<u8>>>
+        body: func() -> list<u8>
     }
-
-    record response {
-        headers: list<tuple<list<u8>, list<u8>>>,
-        body: list<u8>
+    
+    resource response {
+        constructor(headers: list<tuple<list<u8>, list<u8>>>, body: list<u8>)
+        headers: func() -> list<tuple<list<u8>, list<u8>>>
+        body: func() -> list<u8>
     }
-
+    
     enum error {
         bad-request,
     }
+}
+
+interface handler {
+    use types.{request, response, error}
 
     execute: func(req: request) -> result<response, error>
 }
 
+interface logging {
+    resource logger {
+        log: func(message: string)
+    }
+
+    get-logger: func() -> logger
+}
+
+world service-for-host {
+    import logging
+    export handler
+}
+
 world service {
+    import logging
+    export logging
     export handler
 }
 
 world middleware {
+    import logging
     import handler
     export handler
 }
diff --git a/crates/wasm-compose/example/service/Cargo.toml b/crates/wasm-compose/example/service/Cargo.toml
index ba9e6c0e..fde84d1e 100644
--- a/crates/wasm-compose/example/service/Cargo.toml
+++ b/crates/wasm-compose/example/service/Cargo.toml
@@ -5,7 +5,7 @@ edition = "2021"
 publish = false
 
 [dependencies]
-cargo-component-bindings = { git = "https://github.com/bytecodealliance/cargo-component" }
+cargo-component-bindings = "0.4.0"
 
 [lib]
 crate-type = ["cdylib"]
diff --git a/crates/wasm-compose/example/service/src/lib.rs b/crates/wasm-compose/example/service/src/lib.rs
index 03d43e31..6e7bc582 100644
--- a/crates/wasm-compose/example/service/src/lib.rs
+++ b/crates/wasm-compose/example/service/src/lib.rs
@@ -1,15 +1,25 @@
 cargo_component_bindings::generate!();
 
-use bindings::exports::example::service::handler::{Error, Handler, Request, Response};
+use bindings::{
+    example::service::{
+        logging::{get_logger as host_get_logger, Logger as HostLogger},
+        types::{Request, Response},
+    },
+    exports::example::service::{
+        handler::{Error, Guest},
+        logging::{self, GuestLogger},
+    },
+};
+use cargo_component_bindings::rt::Resource;
 use std::str;
 
 struct Component;
 
-impl Handler for Component {
+impl Guest for Component {
     fn execute(req: Request) -> Result<Response, Error> {
+        let headers = req.headers();
         // The content should be plain text
-        let content_type = req
-            .headers
+        let content_type = headers
             .iter()
             .find(|(k, _)| k == b"content-type")
             .map(|(_, v)| v)
@@ -20,12 +30,27 @@ impl Handler for Component {
         }
 
         // We assume the body is UTF-8 encoded
-        let body = str::from_utf8(&req.body).map_err(|_| Error::BadRequest)?;
+        let body = req.body();
+        let body = str::from_utf8(&body).map_err(|_| Error::BadRequest)?;
 
         // Echo the body back in the response
-        Ok(Response {
-            headers: vec![(b"content-type".to_vec(), b"text/plain".to_vec())],
-            body: format!("The request body was: {body}").into_bytes(),
-        })
+        Ok(Response::new(
+            &vec![(b"content-type".to_vec(), b"text/plain".to_vec())],
+            &format!("The request body was: {body}").into_bytes(),
+        ))
+    }
+}
+
+impl logging::Guest for Component {
+    fn get_logger() -> Resource<Logger> {
+        Resource::new(Logger(host_get_logger()))
+    }
+}
+
+pub struct Logger(HostLogger);
+
+impl GuestLogger for Logger {
+    fn log(&self, message: String) {
+        self.0.log(&format!("service logger: {message}"))
     }
 }
diff --git a/crates/wasm-compose/src/composer.rs b/crates/wasm-compose/src/composer.rs
index c0e3d537..66948d09 100644
--- a/crates/wasm-compose/src/composer.rs
+++ b/crates/wasm-compose/src/composer.rs
@@ -202,7 +202,7 @@ impl<'a> CompositionGraphBuilder<'a> {
         types: TypesRef,
     ) -> Result<Option<ExportIndex>> {
         let (instance_name, instance_id) = self.instances.get_index(instance).unwrap();
-        let (_, component) = self.graph.get_component_of_instance(*instance_id).unwrap();
+        let (component_id, component) = self.graph.get_component_of_instance(*instance_id).unwrap();
 
         let (dependent_name, dependent_instance_id) = self.instances.get_index(dependent).unwrap();
 
@@ -215,7 +215,7 @@ impl<'a> CompositionGraphBuilder<'a> {
 
         log::debug!("searching for compatible export from instance `{instance_name}` for argument `{arg_name}` of instance `{dependent_name}`");
 
-        let export = component.find_compatible_export(ty, types).ok_or_else(|| {
+        let export = component.find_compatible_export(ty, types, component_id, &self.graph).ok_or_else(|| {
             anyhow!(
                 "component `{path}` is not compatible with import `{arg_name}` of component `{dependent_path}`",
                 path = component.path().unwrap().display(),
@@ -245,23 +245,29 @@ impl<'a> CompositionGraphBuilder<'a> {
         types: TypesRef,
     ) -> Result<ExportIndex> {
         let (_, instance_id) = self.instances.get_index(instance).unwrap();
-        let (_, component) = self.graph.get_component_of_instance(*instance_id).unwrap();
+        let (component_id, component) = self.graph.get_component_of_instance(*instance_id).unwrap();
+
         match component.export_by_name(export) {
             Some((export_index, kind, index)) if kind == ComponentExternalKind::Instance => {
                 let export_ty = component.types.component_instance_at(index);
-                if !ComponentEntityType::is_subtype_of(
-                    &ComponentEntityType::Instance(export_ty),
+
+                if self.graph.try_connection(
+                    component_id,
+                    ComponentEntityType::Instance(export_ty),
                     component.types(),
-                    &ComponentEntityType::Instance(ty),
+                    ComponentEntityType::Instance(ty),
                     types,
                 ) {
-                    bail!("component `{path}` exports an instance named `{export}` but it is not compatible with import `{arg_name}` of component `{dependent_path}`",
+                    Ok(export_index)
+                } else {
+                    bail!(
+                        "component `{path}` exports an instance named `{export}` \
+                         but it is not compatible with import `{arg_name}` \
+                         of component `{dependent_path}`",
                         path = component.path().unwrap().display(),
                         dependent_path = dependent_path.display(),
                     )
                 }
-
-                Ok(export_index)
             }
             _ => bail!(
                 "component `{path}` does not export an instance named `{export}`",
@@ -497,6 +503,8 @@ impl<'a> CompositionGraphBuilder<'a> {
             }
         }
 
+        self.graph.unify_imported_resources();
+
         Ok((self.instances[root_instance], self.graph))
     }
 }
diff --git a/crates/wasm-compose/src/encoding.rs b/crates/wasm-compose/src/encoding.rs
index 42da8118..18cf7945 100644
--- a/crates/wasm-compose/src/encoding.rs
+++ b/crates/wasm-compose/src/encoding.rs
@@ -17,7 +17,7 @@ use wasmparser::{
     types::{
         self, AnyTypeId, ComponentAnyTypeId, ComponentCoreModuleTypeId, ComponentCoreTypeId,
         ComponentDefinedTypeId, ComponentEntityType, ComponentFuncTypeId, ComponentInstanceTypeId,
-        ComponentTypeId,
+        ComponentTypeId, Remap, Remapping, ResourceId, SubtypeCx,
     },
     ComponentExternalKind,
 };
@@ -110,6 +110,8 @@ pub(crate) struct TypeState<'a> {
 
     // Current type scope that's being translated into.
     cur: TypeScope<'a>,
+
+    remapping: HashMap<ResourceId, (&'a crate::graph::Component<'a>, ResourceId)>,
 }
 
 /// A unique key identifying a type.
@@ -161,6 +163,12 @@ struct TypeScope<'a> {
 
 impl<'a> TypeState<'a> {
     fn new() -> TypeState<'a> {
+        Self::new_with_remapping(HashMap::new())
+    }
+
+    fn new_with_remapping(
+        remapping: HashMap<ResourceId, (&'a crate::graph::Component<'a>, ResourceId)>,
+    ) -> TypeState<'a> {
         TypeState {
             scopes: Vec::new(),
             cur: TypeScope {
@@ -170,6 +178,7 @@ impl<'a> TypeState<'a> {
                 type_defs: HashMap::new(),
                 encodable: Encodable::Builder(Default::default()),
             },
+            remapping,
         }
     }
 
@@ -397,6 +406,16 @@ impl<'a> TypeEncoder<'a> {
             wasmparser::types::ComponentEntityType::Value(ty) => {
                 ComponentTypeRef::Value(self.component_val_type(state, ty))
             }
+            wasmparser::types::ComponentEntityType::Type {
+                created: created @ ComponentAnyTypeId::Resource(_),
+                ..
+            } if AnyTypeId::from(created).peel_alias(&self.0.types).is_none() => {
+                ComponentTypeRef::Type(TypeBounds::SubResource)
+            }
+            wasmparser::types::ComponentEntityType::Type {
+                created: created @ ComponentAnyTypeId::Resource(_),
+                ..
+            } => ComponentTypeRef::Type(TypeBounds::Eq(self.ty(state, created.into()))),
             wasmparser::types::ComponentEntityType::Type { referenced, .. } => {
                 ComponentTypeRef::Type(TypeBounds::Eq(self.ty(state, referenced.into())))
             }
@@ -526,7 +545,6 @@ impl<'a> TypeEncoder<'a> {
 
     fn component_func_type(&self, state: &mut TypeState<'a>, id: ComponentFuncTypeId) -> u32 {
         let ty = &self.0.types[id];
-
         let params = ty
             .params
             .iter()
@@ -573,6 +591,23 @@ impl<'a> TypeEncoder<'a> {
         if let Some(ret) = state.cur.type_defs.get(&key) {
             return *ret;
         }
+
+        // If it's a resource that has been remapped, check one more time using
+        // the remapped version.
+        if let AnyTypeId::Component(ComponentAnyTypeId::Resource(resource)) = id {
+            if let Some((component, id)) = state.remapping.get(&resource.resource()) {
+                let key = (
+                    PtrKey(*component),
+                    AnyTypeId::Component(ComponentAnyTypeId::Resource(
+                        resource.with_resource_id(*id),
+                    )),
+                );
+                if let Some(ret) = state.cur.type_defs.get(&key) {
+                    return *ret;
+                }
+            }
+        }
+
         let idx = self._ty(state, id);
         let prev = state.cur.type_defs.insert(key, idx);
         assert!(prev.is_none());
@@ -582,6 +617,37 @@ impl<'a> TypeEncoder<'a> {
     // Inner version of `ty` above which is a separate method to make it easier
     // to use `return` and not thwart the caching above.
     fn _ty(&self, state: &mut TypeState<'a>, id: AnyTypeId) -> u32 {
+        // Before encoding the type or looking for something to alias, first
+        // determine if the type has already been created but under a different
+        // name. This can happen where one component's view of an imported
+        // instance may be partial and then unioned with another component's
+        // view of an imported instance. The second instance should reuse all
+        // the types defined/exported by the first.
+        //
+        // Here the reverse-export map is consulted where if `key` is an
+        // exported type from this instance (which is registered in "parallel"
+        // when one of those is encountered for all components) then search
+        // with the exported name if any other component has a type definition
+        // for their own version of our `id`.
+        let mut cur = id;
+        loop {
+            let key = (PtrKey(self.0), cur);
+            if let Some(name) = state.cur.type_exports.get(&key) {
+                for key in state.cur.type_exports_rev[name].iter() {
+                    if let Some(ret) = state.cur.type_defs.get(key) {
+                        log::trace!("id already defined through a different component");
+                        return *ret;
+                    }
+                }
+            }
+
+            if let Some(next) = cur.peel_alias(&self.0.types) {
+                cur = next;
+            } else {
+                break;
+            }
+        }
+
         // If `id` is not an alias to anything else then it's a defined type in
         // this scope meaning that it needs to be translated and represented
         // here.
@@ -590,7 +656,9 @@ impl<'a> TypeEncoder<'a> {
                 AnyTypeId::Core(ComponentCoreTypeId::Sub(_)) => unreachable!(),
                 AnyTypeId::Core(ComponentCoreTypeId::Module(id)) => self.module_type(state, id),
                 AnyTypeId::Component(id) => match id {
-                    ComponentAnyTypeId::Resource(_) => unimplemented!(),
+                    ComponentAnyTypeId::Resource(_) => unreachable!(
+                        "should have been handled in `TypeEncoder::component_entity_type`"
+                    ),
                     ComponentAnyTypeId::Defined(id) => self.defined_type(state, id),
                     ComponentAnyTypeId::Func(id) => self.component_func_type(state, id),
                     ComponentAnyTypeId::Instance(id) => self.component_instance_type(state, id),
@@ -599,32 +667,6 @@ impl<'a> TypeEncoder<'a> {
             };
         }
 
-        // Otherwise at this point we know that `id` is an alias to something.
-        // This basically means that it's a reference to an exported type which
-        // is an alias to a different underlying type.
-        //
-        // Before attempting to find something to alias first determine if the
-        // type has already been created but under a different name. This
-        // can happen where one component's view of an imported instance
-        // may be partial and then unioned with another component's view of an
-        // imported instance. The second instance should reuse all the types
-        // defined/exported by the first.
-        //
-        // Here the reverse-export map is consulted where if `key` is an
-        // exported type from this instance (which is registered in "parallel"
-        // when one of those is encountered for all components) then search
-        // with the exported name if any other component has a type definition
-        // for their own version of our `id`.
-        let key = (PtrKey(self.0), id);
-        if let Some(name) = state.cur.type_exports.get(&key) {
-            for key in state.cur.type_exports_rev[name].iter() {
-                if let Some(ret) = state.cur.type_defs.get(key) {
-                    log::trace!("id already defined through a different component");
-                    return *ret;
-                }
-            }
-        }
-
         // Failing all that it seems that this type is defined elsewhere and
         // no one has created an alias yet for it. That's our job so follow the
         // alias chain and search all our outer scopes for one that has an
@@ -663,9 +705,11 @@ impl<'a> TypeEncoder<'a> {
             // in this type translation. It should be the case that we know
             // how to find all types at this point, so reaching the end means
             // there's a missing case one way or another.
-            cur = cur.peel_alias(&self.0.types).unwrap_or_else(|| {
+            let next = cur.peel_alias(&self.0.types).unwrap_or_else(|| {
                 panic!("failed to find alias of {id:?}");
             });
+
+            cur = next;
         }
     }
 
@@ -712,11 +756,17 @@ impl<'a> TypeEncoder<'a> {
             wasmparser::types::ComponentDefinedType::Result { ok, err } => {
                 self.result(state, *ok, *err)
             }
-            wasmparser::types::ComponentDefinedType::Own(_r) => {
-                unimplemented!()
+            wasmparser::types::ComponentDefinedType::Own(r) => {
+                let ty = self.ty(state, (*r).into());
+                let index = state.cur.encodable.type_count();
+                state.cur.encodable.ty().defined_type().own(ty);
+                index
             }
-            wasmparser::types::ComponentDefinedType::Borrow(_r) => {
-                unimplemented!()
+            wasmparser::types::ComponentDefinedType::Borrow(r) => {
+                let ty = self.ty(state, (*r).into());
+                let index = state.cur.encodable.type_count();
+                state.cur.encodable.ty().defined_type().borrow(ty);
+                index
             }
         }
     }
@@ -826,11 +876,14 @@ impl<'a> TypeEncoder<'a> {
         if let Some(id) = id {
             // Update the index in the type map to point to this export
             let key = (PtrKey(self.0), id.into());
-            let prev = state
-                .cur
-                .type_defs
-                .insert(key, state.cur.encodable.type_count());
-            assert!(prev.is_none());
+            let value = state.cur.encodable.type_count();
+            let prev = state.cur.type_defs.insert(key, value);
+            // Resource aliases will already have been added to the table, but
+            // other types should not have been:
+            assert!(
+                prev.is_none()
+                    || (matches!(id, ComponentAnyTypeId::Resource(_)) && prev == Some(value))
+            );
             state.cur.add_type_export(key, name);
         }
         export
@@ -865,7 +918,7 @@ struct ArgumentImport<'a> {
 }
 
 impl ArgumentImport<'_> {
-    fn merge(&mut self, arg: Self) -> Result<()> {
+    fn merge(&mut self, arg: Self, remapping: &mut Remapping) -> Result<()> {
         assert_eq!(self.name, arg.name);
         self.instances.extend(arg.instances);
 
@@ -900,6 +953,7 @@ impl ArgumentImport<'_> {
                             *existing_type,
                             new_component,
                             *new_type,
+                            remapping,
                         ) {
                             continue;
                         }
@@ -938,6 +992,7 @@ impl ArgumentImport<'_> {
                     *existing_type,
                     new_component,
                     new_type,
+                    remapping,
                 ) {
                     bail!(
                         "cannot import {ty} with name `{name}` for an \
@@ -967,18 +1022,25 @@ impl ArgumentImport<'_> {
         existing_type: ComponentEntityType,
         new_component: &'a crate::graph::Component<'a>,
         new_type: ComponentEntityType,
+        remapping: &mut Remapping,
     ) -> bool {
-        ComponentEntityType::is_subtype_of(
-            &existing_type,
-            existing_component.types(),
-            &new_type,
-            new_component.types(),
-        ) && ComponentEntityType::is_subtype_of(
-            &new_type,
-            new_component.types(),
-            &existing_type,
-            existing_component.types(),
-        )
+        let mut context =
+            SubtypeCx::new_with_refs(existing_component.types(), new_component.types());
+        let mut a_ty = existing_type;
+        context.a.remap_component_entity(&mut a_ty, remapping);
+        remapping.reset_type_cache();
+
+        let mut b_ty = new_type;
+        context.b.remap_component_entity(&mut b_ty, remapping);
+        remapping.reset_type_cache();
+
+        if context.component_entity_type(&a_ty, &b_ty, 0).is_err() {
+            return false;
+        }
+
+        context.swap();
+
+        context.component_entity_type(&b_ty, &a_ty, 0).is_ok()
     }
 }
 
@@ -1029,6 +1091,7 @@ impl<'a> ImportMap<'a> {
     }
 
     fn add_instantiation_imports(&mut self, graph: &'a CompositionGraph) -> Result<()> {
+        let remapping = &mut graph.resource_mapping.borrow().remapping();
         let mut imported = HashMap::new();
 
         // Metadata about dependency edges used below during sorting (see
@@ -1093,7 +1156,7 @@ impl<'a> ImportMap<'a> {
                                         );
                                 }
                                 ImportMapEntry::Argument(existing) => {
-                                    existing.merge(arg)?;
+                                    existing.merge(arg, remapping)?;
                                     e.index()
                                 }
                             },
@@ -1208,7 +1271,7 @@ impl DependencyRegistrar<'_, '_> {
         }
 
         match ty {
-            ComponentAnyTypeId::Resource(_) => unimplemented!(),
+            ComponentAnyTypeId::Resource(_) => {}
             ComponentAnyTypeId::Defined(id) => self.defined(id),
             ComponentAnyTypeId::Func(id) => self.func(id),
             ComponentAnyTypeId::Instance(id) => self.instance(id),
@@ -1256,8 +1319,8 @@ impl DependencyRegistrar<'_, '_> {
             types::ComponentDefinedType::List(t) | types::ComponentDefinedType::Option(t) => {
                 self.val_type(*t)
             }
-            types::ComponentDefinedType::Own(_r) | types::ComponentDefinedType::Borrow(_r) => {
-                unimplemented!()
+            types::ComponentDefinedType::Own(r) | types::ComponentDefinedType::Borrow(r) => {
+                self.ty(ComponentAnyTypeId::Resource(*r))
             }
             types::ComponentDefinedType::Record(r) => {
                 for (_, ty) in r.fields.iter() {
@@ -1357,11 +1420,11 @@ impl<'a> CompositionGraphEncoder<'a> {
 
         // Create new state to track where types are defined and in which
         // instances. Temporarily "move" the `encoded` builder into this state.
-        let mut state = TypeState::new();
+        let mut state = TypeState::new_with_remapping(self.graph.remapping_map());
         state.cur.encodable = Encodable::Builder(mem::take(encoded));
 
         for (name, entry) in imports.0 {
-            log::trace!("encoding instance import {name}");
+            log::trace!("encoding import {name}");
             match entry {
                 ImportMapEntry::Component(component) => {
                     let encoded = match &mut state.cur.encodable {
diff --git a/crates/wasm-compose/src/graph.rs b/crates/wasm-compose/src/graph.rs
index 321b61bf..f184d3eb 100644
--- a/crates/wasm-compose/src/graph.rs
+++ b/crates/wasm-compose/src/graph.rs
@@ -5,12 +5,16 @@ use indexmap::{IndexMap, IndexSet};
 use petgraph::{algo::toposort, graphmap::DiGraphMap, EdgeDirection};
 use std::{
     borrow::Cow,
+    cell::RefCell,
     collections::{hash_map::Entry, HashMap, HashSet},
     path::{Path, PathBuf},
     sync::atomic::{AtomicUsize, Ordering},
 };
 use wasmparser::{
-    types::{ComponentEntityType, ComponentInstanceTypeId, Types, TypesRef},
+    types::{
+        ComponentAnyTypeId, ComponentEntityType, ComponentInstanceTypeId, Remap, Remapping,
+        ResourceId, SubtypeCx, Types, TypesRef,
+    },
     Chunk, ComponentExternalKind, ComponentTypeRef, Encoding, Parser, Payload, ValidPayload,
     Validator, WasmFeatures,
 };
@@ -269,6 +273,8 @@ impl<'a> Component<'a> {
         &self,
         ty: ComponentInstanceTypeId,
         types: TypesRef,
+        export_component_id: ComponentId,
+        graph: &CompositionGraph,
     ) -> Option<ExportIndex> {
         self.exports
             .iter()
@@ -276,10 +282,12 @@ impl<'a> Component<'a> {
                 if *kind != ComponentExternalKind::Instance {
                     return false;
                 }
-                ComponentEntityType::is_subtype_of(
-                    &ComponentEntityType::Instance(self.types.component_instance_at(*index)),
+
+                graph.try_connection(
+                    export_component_id,
+                    ComponentEntityType::Instance(self.types.component_instance_at(*index)),
                     self.types(),
-                    &ComponentEntityType::Instance(ty),
+                    ComponentEntityType::Instance(ty),
                     types,
                 )
             })
@@ -438,6 +446,85 @@ pub struct EncodeOptions {
     pub validate: bool,
 }
 
+#[derive(Clone, Debug, Default)]
+pub(crate) struct ResourceMapping {
+    map: im_rc::HashMap<ResourceId, (ComponentId, ResourceId)>,
+}
+
+impl ResourceMapping {
+    fn add_pairs(
+        mut self,
+        export_component: ComponentId,
+        export_type: ComponentEntityType,
+        export_types: TypesRef,
+        import_type: ComponentEntityType,
+        import_types: TypesRef,
+    ) -> Option<Self> {
+        if let (
+            ComponentEntityType::Instance(export_type),
+            ComponentEntityType::Instance(import_type),
+        ) = (export_type, import_type)
+        {
+            let mut exports = HashMap::new();
+            for (export_name, ty) in &export_types[export_type].exports {
+                // TODO: support nested instances
+                if let ComponentEntityType::Type {
+                    referenced: ComponentAnyTypeId::Resource(resource_id),
+                    ..
+                } = ty
+                {
+                    exports.insert(export_name, (export_component, resource_id.resource()));
+                }
+            }
+
+            for (export_name, ty) in &import_types[import_type].exports {
+                // TODO: support nested instances
+                if let ComponentEntityType::Type {
+                    referenced: ComponentAnyTypeId::Resource(resource_id),
+                    ..
+                } = ty
+                {
+                    let import_resource = resource_id.resource();
+                    if let Some((export_component, export_resource)) =
+                        exports.get(&export_name).copied()
+                    {
+                        let value = self
+                            .map
+                            .get(&export_resource)
+                            .copied()
+                            .or_else(|| self.map.get(&import_resource).copied())
+                            .unwrap_or((export_component, export_resource));
+
+                        if value.1 == export_resource {
+                            self.map.insert(export_resource, value);
+                            self.map.insert(import_resource, value);
+                        } else {
+                            // Can't set two different exports equal to each other -- give up.
+                            return None;
+                        }
+                    } else {
+                        // Couldn't find an export with a name that matches this
+                        // import -- give up.
+                        return None;
+                    }
+                }
+            }
+        }
+
+        Some(self)
+    }
+
+    pub(crate) fn remapping(&self) -> Remapping {
+        let mut remapping = Remapping::default();
+        for (old, (_, new)) in &self.map {
+            if old != new {
+                remapping.add(*old, *new)
+            }
+        }
+        remapping
+    }
+}
+
 /// Represents a composition graph used to compose a new component
 /// from other components.
 #[derive(Debug, Default)]
@@ -449,6 +536,7 @@ pub struct CompositionGraph<'a> {
     // An edge between nodes stores a map of target import index to source export index.
     // A source export index of `None` means that the source instance itself is being used.
     pub(crate) graph: DiGraphMap<InstanceId, IndexMap<ImportIndex, Option<ExportIndex>>>,
+    pub(crate) resource_mapping: RefCell<ResourceMapping>,
 }
 
 impl<'a> CompositionGraph<'a> {
@@ -457,6 +545,130 @@ impl<'a> CompositionGraph<'a> {
         Self::default()
     }
 
+    /// Gather any remaining resource imports which have not already been
+    /// connected to exports, group them by name, and update the resource
+    /// mapping to make all resources within each group equivalent.
+    ///
+    /// This should be the last step prior to encoding, after all
+    /// inter-component connections have been made.  It ensures that each set of
+    /// identical imports composed component can be merged into a single import
+    /// in the output component.
+    pub(crate) fn unify_imported_resources(&self) {
+        let mut resource_mapping = self.resource_mapping.borrow_mut();
+
+        let mut resource_imports = HashMap::<_, Vec<_>>::new();
+        for (component_id, component) in &self.components {
+            let component = &component.component;
+            for import_name in component.imports.keys() {
+                let ty = component
+                    .types
+                    .component_entity_type_of_import(import_name)
+                    .unwrap();
+
+                if let ComponentEntityType::Instance(instance_id) = ty {
+                    for (export_name, ty) in &component.types[instance_id].exports {
+                        // TODO: support nested instances
+                        if let ComponentEntityType::Type {
+                            referenced: ComponentAnyTypeId::Resource(resource_id),
+                            ..
+                        } = ty
+                        {
+                            if !resource_mapping.map.contains_key(&resource_id.resource()) {
+                                resource_imports
+                                    .entry(vec![import_name.to_string(), export_name.to_string()])
+                                    .or_default()
+                                    .push((*component_id, resource_id.resource()))
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        for resources in resource_imports.values() {
+            match &resources[..] {
+                [] => unreachable!(),
+                [_] => {}
+                [first, rest @ ..] => {
+                    resource_mapping.map.insert(first.1, *first);
+                    for resource in rest {
+                        resource_mapping.map.insert(resource.1, *first);
+                    }
+                }
+            }
+        }
+    }
+
+    /// Attempt to connect the specified import to the specified export.
+    ///
+    /// This will attempt to match up any resource types by name by name and
+    /// optimistically produce a remapping that sets identically-named pairs
+    /// equal to each other, provided that remapping does not contradict any
+    /// previous remappings.  If the import is not a subtype of the export
+    /// (either because a consistent remapping could not be created or because
+    /// the instances were incompatible for other reasons), we discard the
+    /// remapping changes and return `false`.  Otherwise, we store the remapping
+    /// changes and return `true`.
+    ///
+    /// Note that although this method takes a shared reference, it uses
+    /// internal mutability to update the remapping.
+    pub(crate) fn try_connection(
+        &self,
+        export_component: ComponentId,
+        mut export_type: ComponentEntityType,
+        export_types: TypesRef,
+        mut import_type: ComponentEntityType,
+        import_types: TypesRef,
+    ) -> bool {
+        let resource_mapping = self.resource_mapping.borrow().clone();
+
+        if let Some(resource_mapping) = resource_mapping.add_pairs(
+            export_component,
+            export_type,
+            export_types,
+            import_type,
+            import_types,
+        ) {
+            let remapping = &mut resource_mapping.remapping();
+            let mut context = SubtypeCx::new_with_refs(export_types, import_types);
+
+            context
+                .a
+                .remap_component_entity(&mut export_type, remapping);
+            remapping.reset_type_cache();
+
+            context
+                .b
+                .remap_component_entity(&mut import_type, remapping);
+            remapping.reset_type_cache();
+
+            if context
+                .component_entity_type(&export_type, &import_type, 0)
+                .is_ok()
+            {
+                *self.resource_mapping.borrow_mut() = resource_mapping;
+                true
+            } else {
+                false
+            }
+        } else {
+            false
+        }
+    }
+
+    pub(crate) fn remapping_map<'b>(
+        &'b self,
+    ) -> HashMap<ResourceId, (&'b Component<'a>, ResourceId)> {
+        let mut map = HashMap::new();
+        for (old, (component, new)) in &self.resource_mapping.borrow().map {
+            if old != new {
+                let component = &self.components.get(component).unwrap().component;
+                map.insert(*old, (component, *new));
+            }
+        }
+        map
+    }
+
     /// Adds a new component to the graph.
     ///
     /// The component name must be unique.
@@ -737,14 +949,16 @@ impl<'a> CompositionGraph<'a> {
                 .export_entity_type(export_index)
                 .ok_or_else(|| anyhow!("the source export index is invalid"))?;
 
-            if !ComponentEntityType::is_subtype_of(
-                &export_ty,
+            if !self.try_connection(
+                source_instance.component,
+                export_ty,
                 source_component.types(),
-                &import_ty,
+                import_ty,
                 target_component.types(),
             ) {
                 bail!(
-                    "source {export_ty} export `{export_name}` is not compatible with target {import_ty} import `{import_name}`",
+                    "source {export_ty} export `{export_name}` is not compatible with target \
+                         {import_ty} import `{import_name}`",
                     export_ty = type_desc(export_ty),
                     import_ty = type_desc(import_ty),
                 );
diff --git a/crates/wasmparser/src/validator/types.rs b/crates/wasmparser/src/validator/types.rs
index f774d885..ca01534c 100644
--- a/crates/wasmparser/src/validator/types.rs
+++ b/crates/wasmparser/src/validator/types.rs
@@ -504,6 +504,15 @@ impl Aliasable for AliasableResourceId {
 }
 
 impl AliasableResourceId {
+    /// Create a new instance with the specified resource ID and `self`'s alias
+    /// ID.
+    pub fn with_resource_id(&self, id: ResourceId) -> Self {
+        Self {
+            id,
+            alias_id: self.alias_id,
+        }
+    }
+
     /// Get the underlying resource.
     pub fn resource(&self) -> ResourceId {
         self.id
@@ -1450,7 +1459,7 @@ pub struct ResourceId {
     //
     // The 32-bit storage here should ideally be enough for any component
     // containing resources. If memory usage becomes an issue (this struct is
-    // 12 bytes instead of 8 or 4) then this coudl get folded into the globally
+    // 12 bytes instead of 8 or 4) then this could get folded into the globally
     // unique id with everything using an atomic increment perhaps.
     contextually_unique_id: u32,
 }
@@ -3197,7 +3206,7 @@ impl TypeAlloc {
 ///
 /// This currently exists to abstract over `TypeAlloc` and `SubtypeArena` which
 /// both need to perform remapping operations.
-pub(crate) trait Remap
+pub trait Remap
 where
     Self: Index<ComponentTypeId, Output = ComponentType>,
     Self: Index<ComponentDefinedTypeId, Output = ComponentDefinedType>,
@@ -3210,10 +3219,12 @@ where
     where
         T: TypeData;
 
+    /// Apply `map` to the keys of `tmp`, setting `*any_changed = true` if any
+    /// keys were remapped.
     fn map_map(
         tmp: &mut IndexMap<ResourceId, Vec<usize>>,
         any_changed: &mut bool,
-        map: &mut Remapping,
+        map: &Remapping,
     ) {
         for (id, path) in mem::take(tmp) {
             let id = match map.resources.get(&id) {
@@ -3227,6 +3238,9 @@ where
         }
     }
 
+    /// If `any_changed` is true, push `ty`, update `map` to point `id` to the
+    /// new type ID, set `id` equal to the new type ID, and return `true`.
+    /// Otherwise, update `map` to point `id` to itself and return `false`.
     fn insert_if_any_changed<T>(
         &mut self,
         map: &mut Remapping,
@@ -3245,6 +3259,9 @@ where
         changed
     }
 
+    /// Recursively search for any resource types reachable from `id`, updating
+    /// it and `map` if any are found and remapped, returning `true` iff at last
+    /// one is remapped.
     fn remap_component_any_type_id(
         &mut self,
         id: &mut ComponentAnyTypeId,
@@ -3259,7 +3276,9 @@ where
         }
     }
 
-    fn remap_resource_id(&mut self, id: &mut AliasableResourceId, map: &mut Remapping) -> bool {
+    /// If `map` indicates `id` should be remapped, update it and return `true`.
+    /// Otherwise, do nothing and return `false`.
+    fn remap_resource_id(&mut self, id: &mut AliasableResourceId, map: &Remapping) -> bool {
         if let Some(changed) = map.remap_id(id) {
             return changed;
         }
@@ -3273,6 +3292,9 @@ where
         }
     }
 
+    /// Recursively search for any resource types reachable from `id`, updating
+    /// it and `map` if any are found and remapped, returning `true` iff at last
+    /// one is remapped.
     fn remap_component_type_id(&mut self, id: &mut ComponentTypeId, map: &mut Remapping) -> bool {
         if let Some(changed) = map.remap_id(id) {
             return changed;
@@ -3297,6 +3319,9 @@ where
         self.insert_if_any_changed(map, any_changed, id, ty)
     }
 
+    /// Recursively search for any resource types reachable from `id`, updating
+    /// it and `map` if any are found and remapped, returning `true` iff at last
+    /// one is remapped.
     fn remap_component_defined_type_id(
         &mut self,
         id: &mut ComponentDefinedTypeId,
@@ -3347,6 +3372,9 @@ where
         self.insert_if_any_changed(map, any_changed, id, tmp)
     }
 
+    /// Recursively search for any resource types reachable from `id`, updating
+    /// it and `map` if any are found and remapped, returning `true` iff at last
+    /// one is remapped.
     fn remap_component_instance_type_id(
         &mut self,
         id: &mut ComponentInstanceTypeId,
@@ -3371,6 +3399,9 @@ where
         self.insert_if_any_changed(map, any_changed, id, tmp)
     }
 
+    /// Recursively search for any resource types reachable from `id`, updating
+    /// it and `map` if any are found and remapped, returning `true` iff at last
+    /// one is remapped.
     fn remap_component_func_type_id(
         &mut self,
         id: &mut ComponentFuncTypeId,
@@ -3432,8 +3463,10 @@ where
     }
 }
 
+/// Utility for mapping equivalent `ResourceId`s to each other and (when paired with the `Remap` trait)
+/// non-destructively edit type lists to reflect those mappings.
 #[derive(Debug, Default)]
-pub(crate) struct Remapping {
+pub struct Remapping {
     /// A mapping from old resource ID to new resource ID.
     pub(crate) resources: HashMap<ResourceId, ResourceId>,
 
@@ -3465,6 +3498,16 @@ where
 }
 
 impl Remapping {
+    /// Add a mapping from the specified old resource ID to the new resource ID
+    pub fn add(&mut self, old: ResourceId, new: ResourceId) {
+        self.resources.insert(old, new);
+    }
+
+    /// Clear the type cache while leaving the resource mappings intact.
+    pub fn reset_type_cache(&mut self) {
+        self.types.clear()
+    }
+
     fn remap_id<T>(&self, id: &mut T) -> Option<bool>
     where
         T: Copy + Into<ComponentAnyTypeId> + TryFrom<ComponentAnyTypeId>,
@@ -3499,12 +3542,19 @@ impl Remapping {
 /// Note that this subtyping context also explicitly supports being created
 /// from to different lists `a` and `b` originally, for testing subtyping
 /// between two different components for example.
-pub(crate) struct SubtypeCx<'a> {
-    pub(crate) a: SubtypeArena<'a>,
-    pub(crate) b: SubtypeArena<'a>,
+pub struct SubtypeCx<'a> {
+    /// Lookup arena for first type argument
+    pub a: SubtypeArena<'a>,
+    /// Lookup arena for second type argument
+    pub b: SubtypeArena<'a>,
 }
 
 impl<'a> SubtypeCx<'a> {
+    /// Create a new instance with the specified type lists
+    pub fn new_with_refs(a: TypesRef<'a>, b: TypesRef<'a>) -> SubtypeCx<'a> {
+        Self::new(a.list, b.list)
+    }
+
     pub(crate) fn new(a: &'a TypeList, b: &'a TypeList) -> SubtypeCx<'a> {
         SubtypeCx {
             a: SubtypeArena::new(a),
@@ -3512,7 +3562,8 @@ impl<'a> SubtypeCx<'a> {
         }
     }
 
-    fn swap(&mut self) {
+    /// Swap the type lists
+    pub fn swap(&mut self) {
         mem::swap(&mut self.a, &mut self.b);
     }
 
@@ -3564,6 +3615,9 @@ impl<'a> SubtypeCx<'a> {
         }
     }
 
+    /// Tests whether `a` is a subtype of `b`.
+    ///
+    /// Errors are reported at the `offset` specified.
     pub fn component_type(
         &mut self,
         a: ComponentTypeId,
@@ -3646,6 +3700,9 @@ impl<'a> SubtypeCx<'a> {
         })
     }
 
+    /// Tests whether `a` is a subtype of `b`.
+    ///
+    /// Errors are reported at the `offset` specified.
     pub fn component_instance_type(
         &mut self,
         a_id: ComponentInstanceTypeId,
@@ -3679,6 +3736,9 @@ impl<'a> SubtypeCx<'a> {
         Ok(())
     }
 
+    /// Tests whether `a` is a subtype of `b`.
+    ///
+    /// Errors are reported at the `offset` specified.
     pub fn component_func_type(
         &mut self,
         a: ComponentFuncTypeId,
@@ -3751,6 +3811,9 @@ impl<'a> SubtypeCx<'a> {
         Ok(())
     }
 
+    /// Tests whether `a` is a subtype of `b`.
+    ///
+    /// Errors are reported at the `offset` specified.
     pub fn module_type(
         &mut self,
         a: ComponentCoreModuleTypeId,
@@ -3787,6 +3850,9 @@ impl<'a> SubtypeCx<'a> {
         Ok(())
     }
 
+    /// Tests whether `a` is a subtype of `b`.
+    ///
+    /// Errors are reported at the `offset` specified.
     pub fn component_any_type_id(
         &mut self,
         a: ComponentAnyTypeId,
@@ -3798,7 +3864,12 @@ impl<'a> SubtypeCx<'a> {
                 if a.resource() == b.resource() {
                     Ok(())
                 } else {
-                    bail!(offset, "resource types are not the same")
+                    bail!(
+                        offset,
+                        "resource types are not the same ({:?} vs. {:?})",
+                        a.resource(),
+                        b.resource()
+                    )
                 }
             }
             (ComponentAnyTypeId::Resource(_), b) => {
@@ -4297,7 +4368,7 @@ impl<'a> SubtypeCx<'a> {
 /// This is intended to have arena-like behavior where everything pushed onto
 /// `self.list` is thrown away after a subtyping computation is performed. All
 /// new types pushed into this arena are purely temporary.
-pub(crate) struct SubtypeArena<'a> {
+pub struct SubtypeArena<'a> {
     types: &'a TypeList,
     list: TypeList,
 }


Test patch:
diff --git a/crates/wasm-compose/tests/compositions/connect-resources/composed.wat b/crates/wasm-compose/tests/compositions/connect-resources/composed.wat
new file mode 100644
index 00000000..257d8a86
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/connect-resources/composed.wat
@@ -0,0 +1,147 @@
+(component
+  (type (;0;)
+    (instance
+      (export (;0;) "logger" (type (sub resource)))
+      (type (;1;) (borrow 0))
+      (type (;2;) (func (param "self" 1) (param "message" string)))
+      (export (;0;) "[method]logger.log" (func (type 2)))
+      (type (;3;) (own 0))
+      (type (;4;) (func (result 3)))
+      (export (;1;) "get-logger" (func (type 4)))
+    )
+  )
+  (import "example:service/logging@0.1.0" (instance (;0;) (type 0)))
+  (component (;0;)
+    (type (;0;)
+      (instance
+        (export (;0;) "logger" (type (sub resource)))
+        (type (;1;) (borrow 0))
+        (type (;2;) (func (param "self" 1) (param "message" string)))
+        (export (;0;) "[method]logger.log" (func (type 2)))
+        (type (;3;) (own 0))
+        (type (;4;) (func (result 3)))
+        (export (;1;) "get-logger" (func (type 4)))
+      )
+    )
+    (import "example:service/logging@0.1.0" (instance (;0;) (type 0)))
+  )
+  (component (;1;)
+    (type (;0;)
+      (instance
+        (export (;0;) "logger" (type (sub resource)))
+        (type (;1;) (borrow 0))
+        (type (;2;) (func (param "self" 1) (param "message" string)))
+        (export (;0;) "[method]logger.log" (func (type 2)))
+        (type (;3;) (own 0))
+        (type (;4;) (func (result 3)))
+        (export (;1;) "get-logger" (func (type 4)))
+      )
+    )
+    (import "example:service/logging@0.1.0" (instance $import-logging-instance (;0;) (type 0)))
+    (core module $module (;0;)
+      (type (;0;) (func (param i32 i32 i32)))
+      (type (;1;) (func (result i32)))
+      (type (;2;) (func (param i32) (result i32)))
+      (type (;3;) (func (param i32 i32 i32 i32) (result i32)))
+      (import "example:service/logging@0.1.0" "[method]logger.log" (func (;0;) (type 0)))
+      (import "example:service/logging@0.1.0" "get-logger" (func (;1;) (type 1)))
+      (import "[export]example:service/logging@0.1.0" "[resource-new]logger" (func (;2;) (type 2)))
+      (func $cabi_realloc (;3;) (type 3) (param i32 i32 i32 i32) (result i32)
+        unreachable
+      )
+      (func $logger-log (;4;) (type 0) (param i32 i32 i32)
+        unreachable
+      )
+      (func $get-logger (;5;) (type 1) (result i32)
+        unreachable
+      )
+      (memory $memory (;0;) 1)
+      (export "memory" (memory $memory))
+      (export "cabi_realloc" (func $cabi_realloc))
+      (export "example:service/logging@0.1.0#[method]logger.log" (func $logger-log))
+      (export "example:service/logging@0.1.0#get-logger" (func $get-logger))
+    )
+    (core module $module-indirect (;1;)
+      (type (;0;) (func (param i32 i32 i32)))
+      (type (;1;) (func (result i32)))
+      (type (;2;) (func (param i32)))
+      (func $logger-log (;0;) (type 0) (param i32 i32 i32)
+        unreachable
+      )
+      (func $get-logger (;1;) (type 1) (result i32)
+        unreachable
+      )
+      (func $dtor-logger (;2;) (type 2) (param i32)
+        unreachable
+      )
+      (export "logger.log" (func $logger-log))
+      (export "[dtor]logger" (func $dtor-logger))
+    )
+    (core instance $module-indirect-instance (;0;) (instantiate $module-indirect))
+    (alias export $import-logging-instance "logger" (type $import-logger-type (;1;)))
+    (core func $import-drop-logger (;0;) (canon resource.drop $import-logger-type))
+    (alias core export $module-indirect-instance "logger.log" (core func $import-logger-log (;1;)))
+    (alias export $import-logging-instance "get-logger" (func $import-get-logger (;0;)))
+    (core func $import-get-logger-lowered (;2;) (canon lower (func $import-get-logger)))
+    (core instance $logger-impl-instance (;1;)
+      (export "[resource-drop]logger" (func $import-drop-logger))
+      (export "[method]logger.log" (func $import-logger-log))
+      (export "get-logger" (func $import-get-logger-lowered))
+    )
+    (alias core export $module-indirect-instance "[dtor]logger" (core func $logger-dtor (;3;)))
+    (type $logger-resource (;2;) (resource (rep i32) (dtor (func $logger-dtor))))
+    (core func $logger-new (;4;) (canon resource.new $logger-resource))
+    (core instance $logger-new-instance (;2;)
+      (export "[resource-new]logger" (func $logger-new))
+    )
+    (core instance $module-instance (;3;) (instantiate $module
+        (with "example:service/logging@0.1.0" (instance $logger-impl-instance))
+        (with "[export]example:service/logging@0.1.0" (instance $logger-new-instance))
+      )
+    )
+    (alias core export $module-instance "memory" (core memory $memory (;0;)))
+    (alias core export $module-instance "cabi_realloc" (core func $realloc (;5;)))
+    (type (;3;) (borrow $logger-resource))
+    (type $logger-log-type (;4;) (func (param "self" 3) (param "message" string)))
+    (alias core export $module-instance "example:service/logging@0.1.0#[method]logger.log" (core func $logger-log (;6;)))
+    (func $logger-log-lifted (;1;) (type $logger-log-type) (canon lift (core func $logger-log) (memory $memory) (realloc $realloc) string-encoding=utf8))
+    (alias core export $module-instance "example:service/logging@0.1.0#get-logger" (core func $get-logger (;7;)))
+    (type (;5;) (own $logger-resource))
+    (type $get-logger-type (;6;) (func (result 5)))
+    (func $get-logger-lifted (;2;) (type $get-logger-type) (canon lift (core func $get-logger)))
+    (component $logger (;0;)
+      (import "import-type-logger" (type $import-logger (;0;) (sub resource)))
+      (type (;1;) (borrow $import-logger))
+      (type (;2;) (func (param "self" 1) (param "message" string)))
+      (import "import-method-logger-log" (func $import-logger-log (;0;) (type 2)))
+      (type (;3;) (own $import-logger))
+      (type (;4;) (func (result 3)))
+      (import "import-func-get-logger" (func $import-get-logger (;1;) (type 4)))
+      (export $logger (;5;) "logger" (type $import-logger))
+      (type (;6;) (borrow $logger))
+      (type (;7;) (func (param "self" 6) (param "message" string)))
+      (export (;2;) "[method]logger.log" (func $import-logger-log) (func (type 7)))
+      (type (;8;) (own $logger))
+      (type (;9;) (func (result 8)))
+      (type (;10;) (own $logger))
+      (type (;11;) (func (result 10)))
+      (export (;3;) "get-logger" (func $import-get-logger) (func (type 11)))
+    )
+    (instance $logger-instance (;1;) (instantiate $logger
+        (with "import-method-logger-log" (func $logger-log-lifted))
+        (with "import-func-get-logger" (func $get-logger-lifted))
+        (with "import-type-logger" (type $logger-resource))
+      )
+    )
+    (export (;2;) "example:service/logging@0.1.0" (instance $logger-instance))
+  )
+  (instance (;1;) (instantiate 1
+      (with "example:service/logging@0.1.0" (instance 0))
+    )
+  )
+  (alias export 1 "example:service/logging@0.1.0" (instance (;2;)))
+  (instance (;3;) (instantiate 0
+      (with "example:service/logging@0.1.0" (instance 2))
+    )
+  )
+)
diff --git a/crates/wasm-compose/tests/compositions/connect-resources/config.yml b/crates/wasm-compose/tests/compositions/connect-resources/config.yml
new file mode 100644
index 00000000..96ee0575
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/connect-resources/config.yml
@@ -0,0 +1,6 @@
+instantiations:
+  $input:
+    arguments:
+      example:service/logging@0.1.0:
+        instance: logger
+        export: example:service/logging@0.1.0
diff --git a/crates/wasm-compose/tests/compositions/connect-resources/logger.wat b/crates/wasm-compose/tests/compositions/connect-resources/logger.wat
new file mode 100644
index 00000000..49b51255
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/connect-resources/logger.wat
@@ -0,0 +1,103 @@
+(component
+  (import "example:service/logging@0.1.0"
+    (instance $import-logging-instance
+      (export $logger "logger" (type (sub resource)))
+      (export "[method]logger.log" (func (param "self" (borrow $logger)) (param "message" string)))
+      (export "get-logger" (func (result (own $logger))))
+    )
+  )
+  
+  (core module $module
+    (import "example:service/logging@0.1.0" "[method]logger.log" (func (param i32 i32 i32)))
+    (import "example:service/logging@0.1.0" "get-logger" (func (result i32)))
+    (import "[export]example:service/logging@0.1.0" "[resource-new]logger" (func (param i32) (result i32)))
+    (func $cabi_realloc (param i32 i32 i32 i32) (result i32)
+      unreachable
+    )
+    (func $logger-log (param i32 i32 i32)
+      unreachable
+    )
+    (func $get-logger (result i32)
+      unreachable
+    )
+    (memory $memory 1)
+    (export "memory" (memory $memory))
+    (export "cabi_realloc" (func $cabi_realloc))
+    (export "example:service/logging@0.1.0#[method]logger.log" (func $logger-log))
+    (export "example:service/logging@0.1.0#get-logger" (func $get-logger))
+  )
+
+  (core module $module-indirect
+    (func $logger-log (param i32 i32 i32)
+      unreachable
+    )
+    (func $get-logger (result i32)
+      unreachable
+    )
+    (func $dtor-logger (param i32)
+      unreachable
+    )
+    (export "logger.log" (func $logger-log))
+    (export "[dtor]logger" (func $dtor-logger))
+  )
+  (core instance $module-indirect-instance (instantiate $module-indirect))
+  
+  (alias export $import-logging-instance "logger" (type $import-logger-type))
+  (core func $import-drop-logger (canon resource.drop $import-logger-type))
+  (alias core export $module-indirect-instance "logger.log" (core func $import-logger-log))
+  (alias export $import-logging-instance "get-logger" (func $import-get-logger))
+  (core func $import-get-logger-lowered (canon lower (func $import-get-logger)))
+  (core instance $logger-impl-instance
+    (export "[resource-drop]logger" (func $import-drop-logger))
+    (export "[method]logger.log" (func $import-logger-log))
+    (export "get-logger" (func $import-get-logger-lowered))
+  )
+  
+  (alias core export $module-indirect-instance "[dtor]logger" (core func $logger-dtor))
+  (type $logger-resource (resource (rep i32) (dtor (func $logger-dtor))))
+  (core func $logger-new (canon resource.new $logger-resource))
+  (core instance $logger-new-instance
+    (export "[resource-new]logger" (func $logger-new))
+  )
+  
+  (core instance $module-instance
+    (instantiate $module
+      (with "example:service/logging@0.1.0" (instance $logger-impl-instance))
+      (with "[export]example:service/logging@0.1.0" (instance $logger-new-instance))
+    )
+  )
+  (alias core export $module-instance "memory" (core memory $memory))
+  (alias core export $module-instance "cabi_realloc" (core func $realloc))
+  
+  (type $logger-log-type (func (param "self" (borrow $logger-resource)) (param "message" string)))
+  (alias core export $module-instance "example:service/logging@0.1.0#[method]logger.log" (core func $logger-log))
+  (func $logger-log-lifted
+    (type $logger-log-type)
+    (canon lift (core func $logger-log) (memory $memory) (realloc $realloc) string-encoding=utf8)
+  )
+  (alias core export $module-instance "example:service/logging@0.1.0#get-logger" (core func $get-logger))
+  (type $get-logger-type (func (result (own $logger-resource))))
+  (func $get-logger-lifted (type $get-logger-type) (canon lift (core func $get-logger)))
+
+  (component $logger
+    (import "import-type-logger" (type $import-logger (sub resource)))
+    (import "import-method-logger-log"
+      (func $import-logger-log (param "self" (borrow $import-logger)) (param "message" string))
+    )
+    (import "import-func-get-logger" (func $import-get-logger (result (own $import-logger))))
+    (export $logger "logger" (type $import-logger))
+    (export "[method]logger.log" (func 0) (func (param "self" (borrow $logger)) (param "message" string)))
+    (type (;8;) (own 5))
+    (type (;9;) (func (result 8)))
+    (export (;3;) "get-logger" (func $import-get-logger) (func (result (own $logger))))
+  )
+
+  (instance $logger-instance (instantiate $logger
+      (with "import-method-logger-log" (func $logger-log-lifted))
+      (with "import-func-get-logger" (func $get-logger-lifted))
+      (with "import-type-logger" (type $logger-resource))
+    )
+  )
+  
+  (export "example:service/logging@0.1.0" (instance $logger-instance))
+)
diff --git a/crates/wasm-compose/tests/compositions/connect-resources/root.wat b/crates/wasm-compose/tests/compositions/connect-resources/root.wat
new file mode 100644
index 00000000..2089f620
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/connect-resources/root.wat
@@ -0,0 +1,9 @@
+(component
+  (import "example:service/logging@0.1.0"
+    (instance
+      (export $logger "logger" (type (sub resource)))
+      (export "[method]logger.log" (func (param "self" (borrow $logger)) (param "message" string)))
+      (export "get-logger" (func (result (own $logger))))
+    )
+  )
+)
diff --git a/crates/wasm-compose/tests/compositions/merged-resource-import/b.wat b/crates/wasm-compose/tests/compositions/merged-resource-import/b.wat
new file mode 100644
index 00000000..7c9dd66e
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/merged-resource-import/b.wat
@@ -0,0 +1,9 @@
+(component
+  (import "a"
+    (instance
+      (export "a" (type (sub resource)))
+      (export $b "b" (type (sub resource)))
+      (export "[constructor]b" (func (param "y" u32) (result (own $b))))
+    )
+  )
+)
diff --git a/crates/wasm-compose/tests/compositions/merged-resource-import/composed.wat b/crates/wasm-compose/tests/compositions/merged-resource-import/composed.wat
new file mode 100644
index 00000000..c49be319
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/merged-resource-import/composed.wat
@@ -0,0 +1,48 @@
+(component
+  (type (;0;)
+    (instance
+      (export (;0;) "b" (type (sub resource)))
+      (type (;1;) (own 0))
+      (type (;2;) (func (param "y" u32) (result 1)))
+      (export (;0;) "[constructor]b" (func (type 2)))
+      (export (;3;) "a" (type (sub resource)))
+    )
+  )
+  (import "a" (instance (;0;) (type 0)))
+  (component (;0;)
+    (type (;0;)
+      (instance
+        (export (;0;) "b" (type (sub resource)))
+        (type (;1;) (own 0))
+        (type (;2;) (func (param "y" u32) (result 1)))
+        (export (;0;) "[constructor]b" (func (type 2)))
+      )
+    )
+    (import "a" (instance (;0;) (type 0)))
+    (type (;1;)
+      (instance)
+    )
+    (import "b" (instance (;1;) (type 1)))
+  )
+  (component (;1;)
+    (type (;0;)
+      (instance
+        (export (;0;) "a" (type (sub resource)))
+        (export (;1;) "b" (type (sub resource)))
+        (type (;2;) (own 1))
+        (type (;3;) (func (param "y" u32) (result 2)))
+        (export (;0;) "[constructor]b" (func (type 3)))
+      )
+    )
+    (import "a" (instance (;0;) (type 0)))
+  )
+  (instance (;1;) (instantiate 1
+      (with "a" (instance 0))
+    )
+  )
+  (instance (;2;) (instantiate 0
+      (with "b" (instance 1))
+      (with "a" (instance 0))
+    )
+  )
+)
diff --git a/crates/wasm-compose/tests/compositions/merged-resource-import/root.wat b/crates/wasm-compose/tests/compositions/merged-resource-import/root.wat
new file mode 100644
index 00000000..b3d95407
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/merged-resource-import/root.wat
@@ -0,0 +1,9 @@
+(component
+  (import "a"
+    (instance
+      (export $b "b" (type (sub resource)))
+      (export "[constructor]b" (func (param "y" u32) (result (own $b))))
+    )
+  )
+  (import "b" (instance))
+)