Repository: bytecodealliance/wasm-tools
Pull request #1225 (2023-09-28 15:46:22)

wast: Add exnref, try_table, and throw_ref
These types and instructions are part of the exception handling rework tracked in (https://github.com/WebAssembly/exception-handling/issues/281).

eqrion: Just did the rustfmt, but hold up on merging there's been some recent bikeshedding of the binary to work through. I will probably update again.

Patch:
diff --git a/crates/wast/src/component/binary.rs b/crates/wast/src/component/binary.rs
index de12be2e..7edb4c2b 100644
--- a/crates/wast/src/component/binary.rs
+++ b/crates/wast/src/component/binary.rs
@@ -599,6 +599,9 @@ impl From<core::HeapType<'_>> for wasm_encoder::HeapType {
         match r {
             core::HeapType::Func => Self::Func,
             core::HeapType::Extern => Self::Extern,
+            core::HeapType::Exn => {
+                todo!("encoding of exceptions proposal types not yet implemented")
+            }
             core::HeapType::Index(Index::Num(i, _)) => Self::Indexed(i),
             core::HeapType::Index(_) => panic!("unresolved index"),
             core::HeapType::Any
diff --git a/crates/wast/src/component/resolve.rs b/crates/wast/src/component/resolve.rs
index c0df488f..3e3169aa 100644
--- a/crates/wast/src/component/resolve.rs
+++ b/crates/wast/src/component/resolve.rs
@@ -523,6 +523,7 @@ impl<'a> Resolver<'a> {
                     ValType::Ref(r) => match &mut r.heap {
                         core::HeapType::Func
                         | core::HeapType::Extern
+                        | core::HeapType::Exn
                         | core::HeapType::Any
                         | core::HeapType::Eq
                         | core::HeapType::Array
diff --git a/crates/wast/src/core/binary.rs b/crates/wast/src/core/binary.rs
index 01d3d640..3cfe18a3 100644
--- a/crates/wast/src/core/binary.rs
+++ b/crates/wast/src/core/binary.rs
@@ -246,6 +246,7 @@ impl<'a> Encode for HeapType<'a> {
         match self {
             HeapType::Func => e.push(0x70),
             HeapType::Extern => e.push(0x6f),
+            HeapType::Exn => e.push(0x69),
             HeapType::Any => e.push(0x6e),
             HeapType::Eq => e.push(0x6d),
             HeapType::Struct => e.push(0x6b),
@@ -277,6 +278,11 @@ impl<'a> Encode for RefType<'a> {
                 nullable: true,
                 heap: HeapType::Extern,
             } => e.push(0x6f),
+            // The 'exnref' binary abbreviation
+            RefType {
+                nullable: true,
+                heap: HeapType::Exn,
+            } => e.push(0x69),
             // The 'eqref' binary abbreviation
             RefType {
                 nullable: true,
@@ -1028,6 +1034,47 @@ impl Encode for Id<'_> {
     }
 }
 
+impl<'a> Encode for TryTable<'a> {
+    fn encode(&self, dst: &mut Vec<u8>) {
+        self.block.encode(dst);
+        self.catches.encode(dst);
+
+        let catch_all_flag_byte: u8 = match self.catch_all {
+            None => 0,
+            Some(TryTableCatchAll {
+                kind: TryTableCatchKind::Drop,
+                ..
+            }) => 1,
+            Some(TryTableCatchAll {
+                kind: TryTableCatchKind::Ref,
+                ..
+            }) => 2,
+        };
+        catch_all_flag_byte.encode(dst);
+        if let Some(catch_all) = &self.catch_all {
+            catch_all.encode(dst);
+        }
+    }
+}
+
+impl<'a> Encode for TryTableCatch<'a> {
+    fn encode(&self, dst: &mut Vec<u8>) {
+        let catch_flag_byte: u8 = match self.kind {
+            TryTableCatchKind::Drop => 0,
+            TryTableCatchKind::Ref => 1,
+        };
+        catch_flag_byte.encode(dst);
+        self.tag.encode(dst);
+        self.label.encode(dst);
+    }
+}
+
+impl<'a> Encode for TryTableCatchAll<'a> {
+    fn encode(&self, dst: &mut Vec<u8>) {
+        self.label.encode(dst);
+    }
+}
+
 impl Encode for V128Const {
     fn encode(&self, dst: &mut Vec<u8>) {
         dst.extend_from_slice(&self.to_le_bytes());
diff --git a/crates/wast/src/core/expr.rs b/crates/wast/src/core/expr.rs
index f89624e2..58e8c439 100644
--- a/crates/wast/src/core/expr.rs
+++ b/crates/wast/src/core/expr.rs
@@ -1149,6 +1149,10 @@ instructions! {
         Delegate(Index<'a>) : [0x18] : "delegate",
         CatchAll : [0x19] : "catch_all",
 
+        // Exception handling proposal extension for 'exnref'
+        ThrowRef : [0x0a] : "throw_ref",
+        TryTable(TryTable<'a>) : [0x1f] : "try_table",
+
         // Relaxed SIMD proposal
         I8x16RelaxedSwizzle : [0xfd, 0x100]: "i8x16.relaxed_swizzle",
         I32x4RelaxedTruncF32x4S : [0xfd, 0x101]: "i32x4.relaxed_trunc_f32x4_s",
@@ -1219,6 +1223,85 @@ impl<'a> Parse<'a> for BlockType<'a> {
     }
 }
 
+#[derive(Debug)]
+#[allow(missing_docs)]
+pub struct TryTable<'a> {
+    pub block: Box<BlockType<'a>>,
+    pub catches: Vec<TryTableCatch<'a>>,
+    pub catch_all: Option<TryTableCatchAll<'a>>,
+}
+
+impl<'a> Parse<'a> for TryTable<'a> {
+    fn parse(parser: Parser<'a>) -> Result<Self> {
+        let block = parser.parse()?;
+
+        let mut catches = Vec::new();
+        while parser.peek2::<kw::catch>()? || parser.peek2::<kw::catch_ref>()? {
+            catches.push(parser.parens(|p| {
+                let kind = if parser.peek::<kw::catch_ref>()? {
+                    p.parse::<kw::catch_ref>()?;
+                    TryTableCatchKind::Ref
+                } else {
+                    p.parse::<kw::catch>()?;
+                    TryTableCatchKind::Drop
+                };
+                Ok(TryTableCatch {
+                    kind,
+                    tag: p.parse()?,
+                    label: p.parse()?,
+                })
+            })?);
+        }
+
+        let mut catch_all = None;
+        if parser.peek2::<kw::catch_all>()? || parser.peek2::<kw::catch_all_ref>()? {
+            catch_all = Some(parser.parens(|p| {
+                let kind = if parser.peek::<kw::catch_all_ref>()? {
+                    p.parse::<kw::catch_all_ref>()?;
+                    TryTableCatchKind::Ref
+                } else {
+                    p.parse::<kw::catch_all>()?;
+                    TryTableCatchKind::Drop
+                };
+                Ok(TryTableCatchAll {
+                    kind,
+                    label: p.parse()?,
+                })
+            })?);
+        }
+
+        Ok(TryTable {
+            block,
+            catches,
+            catch_all,
+        })
+    }
+}
+
+#[derive(Debug)]
+#[allow(missing_docs)]
+pub enum TryTableCatchKind {
+    // Capture the exnref for the try
+    Ref,
+    // Drop the exnref for the try
+    Drop,
+}
+
+#[derive(Debug)]
+#[allow(missing_docs)]
+pub struct TryTableCatch<'a> {
+    pub kind: TryTableCatchKind,
+    pub tag: Index<'a>,
+    pub label: Index<'a>,
+}
+
+#[derive(Debug)]
+#[allow(missing_docs)]
+pub struct TryTableCatchAll<'a> {
+    pub kind: TryTableCatchKind,
+    pub label: Index<'a>,
+}
+
 /// Extra information associated with the func.bind instruction.
 #[derive(Debug)]
 #[allow(missing_docs)]
diff --git a/crates/wast/src/core/resolve/names.rs b/crates/wast/src/core/resolve/names.rs
index a82789d7..4f252ad4 100644
--- a/crates/wast/src/core/resolve/names.rs
+++ b/crates/wast/src/core/resolve/names.rs
@@ -521,6 +521,20 @@ impl<'a, 'b> ExprResolver<'a, 'b> {
                 });
                 self.resolve_block_type(bt)?;
             }
+            TryTable(try_table) => {
+                self.blocks.push(ExprBlock {
+                    label: try_table.block.label,
+                    pushed_scope: false,
+                });
+                self.resolve_block_type(&mut try_table.block)?;
+                for catch in &mut try_table.catches {
+                    self.resolver.resolve(&mut catch.tag, Ns::Tag)?;
+                    self.resolve_label(&mut catch.label)?;
+                }
+                if let Some(catch_all) = &mut try_table.catch_all {
+                    self.resolve_label(&mut catch_all.label)?;
+                }
+            }
 
             // On `End` instructions we pop a label from the stack, and for both
             // `End` and `Else` instructions if they have labels listed we
diff --git a/crates/wast/src/core/resolve/types.rs b/crates/wast/src/core/resolve/types.rs
index aaf2988c..bfb996b4 100644
--- a/crates/wast/src/core/resolve/types.rs
+++ b/crates/wast/src/core/resolve/types.rs
@@ -140,7 +140,8 @@ impl<'a> Expander<'a> {
             | Instruction::If(bt)
             | Instruction::Loop(bt)
             | Instruction::Let(LetType { block: bt, .. })
-            | Instruction::Try(bt) => {
+            | Instruction::Try(bt)
+            | Instruction::TryTable(TryTable { block: bt, .. }) => {
                 // No expansion necessary, a type reference is already here.
                 // We'll verify that it's the same as the inline type, if any,
                 // later.
diff --git a/crates/wast/src/core/types.rs b/crates/wast/src/core/types.rs
index e42e295d..4bf69fe0 100644
--- a/crates/wast/src/core/types.rs
+++ b/crates/wast/src/core/types.rs
@@ -66,6 +66,8 @@ pub enum HeapType<'a> {
     /// A reference to any host value: externref. This is part of the reference
     /// types proposal.
     Extern,
+    /// A reference to a wasm exception. This is part of the exceptions proposal.
+    Exn,
     /// A reference to any reference value: anyref. This is part of the GC
     /// proposal.
     Any,
@@ -98,6 +100,9 @@ impl<'a> Parse<'a> for HeapType<'a> {
         } else if l.peek::<kw::r#extern>()? {
             parser.parse::<kw::r#extern>()?;
             Ok(HeapType::Extern)
+        } else if l.peek::<kw::exn>()? {
+            parser.parse::<kw::exn>()?;
+            Ok(HeapType::Exn)
         } else if l.peek::<kw::r#any>()? {
             parser.parse::<kw::r#any>()?;
             Ok(HeapType::Any)
@@ -134,6 +139,7 @@ impl<'a> Peek for HeapType<'a> {
     fn peek(cursor: Cursor<'_>) -> Result<bool> {
         Ok(kw::func::peek(cursor)?
             || kw::r#extern::peek(cursor)?
+            || kw::exn::peek(cursor)?
             || kw::any::peek(cursor)?
             || kw::eq::peek(cursor)?
             || kw::r#struct::peek(cursor)?
@@ -174,6 +180,14 @@ impl<'a> RefType<'a> {
         }
     }
 
+    /// An `exnrefr` as an abbreviation for `(ref null exn)`.
+    pub fn exn() -> Self {
+        RefType {
+            nullable: true,
+            heap: HeapType::Exn,
+        }
+    }
+
     /// An `anyref` as an abbreviation for `(ref null any)`.
     pub fn any() -> Self {
         RefType {
@@ -251,6 +265,9 @@ impl<'a> Parse<'a> for RefType<'a> {
         } else if l.peek::<kw::externref>()? {
             parser.parse::<kw::externref>()?;
             Ok(RefType::r#extern())
+        } else if l.peek::<kw::exnref>()? {
+            parser.parse::<kw::exnref>()?;
+            Ok(RefType::exn())
         } else if l.peek::<kw::anyref>()? {
             parser.parse::<kw::anyref>()?;
             Ok(RefType::any())
@@ -306,6 +323,7 @@ impl<'a> Peek for RefType<'a> {
         Ok(kw::funcref::peek(cursor)?
             || /* legacy */ kw::anyfunc::peek(cursor)?
             || kw::externref::peek(cursor)?
+            || kw::exnref::peek(cursor)?
             || kw::anyref::peek(cursor)?
             || kw::eqref::peek(cursor)?
             || kw::structref::peek(cursor)?
diff --git a/crates/wast/src/lib.rs b/crates/wast/src/lib.rs
index 220e955f..c74d1865 100644
--- a/crates/wast/src/lib.rs
+++ b/crates/wast/src/lib.rs
@@ -393,7 +393,9 @@ pub mod kw {
     custom_keyword!(block);
     custom_keyword!(borrow);
     custom_keyword!(catch);
+    custom_keyword!(catch_ref);
     custom_keyword!(catch_all);
+    custom_keyword!(catch_all_ref);
     custom_keyword!(code);
     custom_keyword!(component);
     custom_keyword!(data);
@@ -404,6 +406,8 @@ pub mod kw {
     custom_keyword!(elem);
     custom_keyword!(end);
     custom_keyword!(tag);
+    custom_keyword!(exn);
+    custom_keyword!(exnref);
     custom_keyword!(export);
     custom_keyword!(r#extern = "extern");
     custom_keyword!(externref);
diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs
index 4b7a7a15..598aa85a 100644
--- a/crates/wit-component/src/encoding.rs
+++ b/crates/wit-component/src/encoding.rs
@@ -74,7 +74,8 @@
 use crate::encoding::world::WorldAdapter;
 use crate::metadata::{self, Bindgen, ModuleMetadata};
 use crate::validation::{
-    ValidatedModule, BARE_FUNC_MODULE_NAME, MAIN_MODULE_IMPORT_NAME, POST_RETURN_PREFIX,
+    ResourceInfo, ValidatedModule, BARE_FUNC_MODULE_NAME, MAIN_MODULE_IMPORT_NAME,
+    POST_RETURN_PREFIX,
 };
 use crate::StringEncoding;
 use anyhow::{anyhow, bail, Context, Result};
@@ -585,43 +586,12 @@ impl<'a> EncodingState<'a> {
             args.push((*adapter, ModuleArg::Instance(index)));
         }
 
-        for (import, info) in info.required_resource_funcs.iter() {
-            let mut exports = Vec::new();
-            for (resource, info) in info {
-                // Destructors for resources live on the shim module previously
-                // created, so if one is specified create the resource with
-                // the shim module that currently exists. The shim will get
-                // filled in later with the actual destructor after the main
-                // module is instantiated.
-                let dtor = info.dtor_export.map(|_| {
-                    self.component.core_alias_export(
-                        self.shim_instance_index.unwrap(),
-                        &shims.shim_names[&ShimKind::ResourceDtor { import, resource }],
-                        ExportKind::Func,
-                    )
-                });
-                let resource_idx = self.component.type_resource(ValType::I32, dtor);
-                let prev = self.export_type_map.insert(info.id, resource_idx);
-                assert!(prev.is_none());
-
-                if let Some(name) = info.drop_import {
-                    let index = self.component.resource_drop(resource_idx);
-                    exports.push((name, ExportKind::Func, index));
-                }
-                if let Some(name) = info.rep_import {
-                    let index = self.component.resource_rep(resource_idx);
-                    exports.push((name, ExportKind::Func, index));
-                }
-                if let Some(name) = info.new_import {
-                    let index = self.component.resource_new(resource_idx);
-                    exports.push((name, ExportKind::Func, index));
-                }
-            }
-            if !exports.is_empty() {
-                let index = self.component.core_instantiate_exports(exports);
-                args.push((import.as_str(), ModuleArg::Instance(index)));
-            }
-        }
+        self.add_resource_funcs(
+            CustomModule::Main,
+            &info.required_resource_funcs,
+            &shims,
+            &mut args,
+        );
 
         // Instantiate the main module now that all of its arguments have been
         // prepared. With this we now have the main linear memory for
@@ -1189,6 +1159,14 @@ impl<'a> EncodingState<'a> {
                     &mut signatures,
                 );
             }
+
+            self.encode_resource_dtors(
+                CustomModule::Adapter(adapter_name),
+                &adapter.info.required_resource_funcs,
+                &mut signatures,
+                &mut ret,
+            );
+
             let funcs = match self.info.info.adapters_required.get(adapter_name) {
                 Some(funcs) => funcs,
                 None => continue,
@@ -1217,31 +1195,12 @@ impl<'a> EncodingState<'a> {
             }
         }
 
-        // Any resource destructors are encoded through the shim module. The
-        // core wasm probably imports resource intrinsics which requires the
-        // resource definition, but the resource definition requires
-        // the destructor to be available. The shim module breaks this
-        // circular dependency.
-        for (import, info) in self.info.info.required_resource_funcs.iter() {
-            for (resource, info) in info {
-                if info.dtor_export.is_none() {
-                    continue;
-                }
-                signatures.push(WasmSignature {
-                    params: vec![WasmType::I32],
-                    results: Vec::new(),
-                    indirect_params: false,
-                    retptr: false,
-                });
-                let name = ret.list.len().to_string();
-                ret.list.push(Shim {
-                    name,
-                    debug_name: format!("dtor-{import}-{resource}"),
-                    options: RequiredOptions::empty(),
-                    kind: ShimKind::ResourceDtor { import, resource },
-                });
-            }
-        }
+        self.encode_resource_dtors(
+            CustomModule::Main,
+            &self.info.info.required_resource_funcs,
+            &mut signatures,
+            &mut ret,
+        );
 
         if ret.list.is_empty() {
             return ret;
@@ -1445,13 +1404,24 @@ impl<'a> EncodingState<'a> {
                 // meaning that any destructor for the resource must be called
                 // indirectly due to the otherwise circular dependency between
                 // the module and the resource itself.
-                ShimKind::ResourceDtor { import, resource } => self.component.core_alias_export(
-                    self.instance_index.unwrap(),
-                    self.info.info.required_resource_funcs[*import][*resource]
-                        .dtor_export
-                        .unwrap(),
-                    ExportKind::Func,
-                ),
+                ShimKind::ResourceDtor {
+                    module,
+                    import,
+                    resource,
+                } => {
+                    let funcs = match module {
+                        CustomModule::Main => &self.info.info.required_resource_funcs,
+                        CustomModule::Adapter(name) => {
+                            &self.info.adapters[name].info.required_resource_funcs
+                        }
+                    };
+
+                    self.component.core_alias_export(
+                        self.instance_index.unwrap(),
+                        funcs[*import][*resource].dtor_export.as_deref().unwrap(),
+                        ExportKind::Func,
+                    )
+                }
             };
 
             exports.push((shim.name.as_str(), ExportKind::Func, core_func_index));
@@ -1495,6 +1465,94 @@ impl<'a> EncodingState<'a> {
         self.instance_index = Some(instance_index);
     }
 
+    fn encode_resource_dtors<'b>(
+        &mut self,
+        module: CustomModule<'b>,
+        funcs: &'b IndexMap<String, IndexMap<String, ResourceInfo>>,
+        signatures: &mut Vec<WasmSignature>,
+        shims: &mut Shims<'b>,
+    ) {
+        // Any resource destructors are encoded through the shim module. The
+        // core wasm probably imports resource intrinsics which requires the
+        // resource definition, but the resource definition requires
+        // the destructor to be available. The shim module breaks this
+        // circular dependency.
+        for (import, info) in funcs.iter() {
+            for (resource, info) in info {
+                if info.dtor_export.is_none() {
+                    continue;
+                }
+                signatures.push(WasmSignature {
+                    params: vec![WasmType::I32],
+                    results: Vec::new(),
+                    indirect_params: false,
+                    retptr: false,
+                });
+                let name = shims.list.len().to_string();
+                shims.list.push(Shim {
+                    name,
+                    debug_name: format!("dtor-{import}-{resource}"),
+                    options: RequiredOptions::empty(),
+                    kind: ShimKind::ResourceDtor {
+                        module,
+                        import,
+                        resource,
+                    },
+                });
+            }
+        }
+    }
+
+    fn add_resource_funcs<'b>(
+        &mut self,
+        module: CustomModule<'b>,
+        funcs: &'b IndexMap<String, IndexMap<String, ResourceInfo>>,
+        shims: &Shims,
+        args: &mut Vec<(&'b str, ModuleArg)>,
+    ) {
+        for (import, info) in funcs {
+            let mut exports = Vec::new();
+            for (resource, info) in info {
+                // Destructors for resources live on the shim module previously
+                // created, so if one is specified create the resource with
+                // the shim module that currently exists. The shim will get
+                // filled in later with the actual destructor after the main
+                // module is instantiated.
+                let dtor = info.dtor_export.as_deref().map(|_| {
+                    self.component.core_alias_export(
+                        self.shim_instance_index.unwrap(),
+                        &shims.shim_names[&ShimKind::ResourceDtor {
+                            module,
+                            import,
+                            resource,
+                        }],
+                        ExportKind::Func,
+                    )
+                });
+                let resource_idx = self.component.type_resource(ValType::I32, dtor);
+                let prev = self.export_type_map.insert(info.id, resource_idx);
+                assert!(prev.is_none());
+
+                if let Some(name) = info.drop_import.as_deref() {
+                    let index = self.component.resource_drop(resource_idx);
+                    exports.push((name, ExportKind::Func, index));
+                }
+                if let Some(name) = info.rep_import.as_deref() {
+                    let index = self.component.resource_rep(resource_idx);
+                    exports.push((name, ExportKind::Func, index));
+                }
+                if let Some(name) = info.new_import.as_deref() {
+                    let index = self.component.resource_new(resource_idx);
+                    exports.push((name, ExportKind::Func, index));
+                }
+            }
+            if !exports.is_empty() {
+                let index = self.component.core_instantiate_exports(exports);
+                args.push((import.as_str(), ModuleArg::Instance(index)));
+            }
+        }
+    }
+
     /// This function will instantiate the specified adapter module, which may
     /// depend on previously-instantiated modules.
     fn instantiate_adapter_module(
@@ -1597,6 +1655,14 @@ impl<'a> EncodingState<'a> {
             );
             args.push((import_name, ModuleArg::Instance(instance)));
         }
+
+        self.add_resource_funcs(
+            CustomModule::Adapter(name),
+            &adapter.info.required_resource_funcs,
+            shims,
+            &mut args,
+        );
+
         let instance = self
             .component
             .core_instantiate(self.adapter_modules[name], args);
@@ -1685,6 +1751,8 @@ enum ShimKind<'a> {
     /// A shim used as the destructor for a resource which allows defining the
     /// resource before the core module being instantiated.
     ResourceDtor {
+        /// Which instance to pull the destructor function from.
+        module: CustomModule<'a>,
         /// The import that the resource was defined for.
         import: &'a str,
         /// The name of the resource being destroyed.
diff --git a/crates/wit-component/src/encoding/types.rs b/crates/wit-component/src/encoding/types.rs
index 00140673..d660612e 100644
--- a/crates/wit-component/src/encoding/types.rs
+++ b/crates/wit-component/src/encoding/types.rs
@@ -131,7 +131,7 @@ pub trait ValtypeEncoder<'a> {
                 // If this type is imported from another interface then return
                 // it as it was bound here with an alias.
                 let ty = &resolve.types[id];
-                log::trace!("encode type name={:?}", ty.name);
+                log::trace!("encode type name={:?} {:?}", ty.name, &ty.kind);
                 if let Some(index) = self.maybe_import_type(resolve, id) {
                     self.type_map().insert(id, index);
                     return Ok(ComponentValType::Type(index));
diff --git a/crates/wit-component/src/encoding/world.rs b/crates/wit-component/src/encoding/world.rs
index 39120c9f..79c9c52e 100644
--- a/crates/wit-component/src/encoding/world.rs
+++ b/crates/wit-component/src/encoding/world.rs
@@ -113,14 +113,29 @@ impl<'a> ComponentWorld<'a> {
         ) in self.encoder.adapters.iter()
         {
             let required_by_import = self.info.adapters_required.get(name.as_str());
-            let required =
-                self.required_adapter_exports(resolve, world, required_exports, required_by_import);
-            if required.is_empty() && library_info.is_none() {
+            let no_required_by_import = || required_by_import.map(|m| m.is_empty()).unwrap_or(true);
+            let no_required_exports = || {
+                required_exports
+                    .iter()
+                    .all(|name| match &resolve.worlds[world].exports[name] {
+                        WorldItem::Function(_) => false,
+                        WorldItem::Interface(id) => resolve.interfaces[*id].functions.is_empty(),
+                        WorldItem::Type(_) => true,
+                    })
+            };
+            if no_required_by_import() && no_required_exports() && library_info.is_none() {
                 continue;
             }
             let wasm = if library_info.is_some() {
                 Cow::Borrowed(wasm as &[u8])
             } else {
+                let required = self.required_adapter_exports(
+                    resolve,
+                    world,
+                    required_exports,
+                    required_by_import,
+                );
+
                 Cow::Owned(
                     crate::gc::run(wasm, &required, self.info.realloc)
                         .context("failed to reduce input adapter module to its minimal size")?,
@@ -131,7 +146,8 @@ impl<'a> ComponentWorld<'a> {
                 resolve,
                 world,
                 metadata,
-                &required,
+                required_by_import,
+                required_exports,
                 library_info.is_some(),
             )
             .context("failed to validate the imports of the minimized adapter module")?;
diff --git a/crates/wit-component/src/validation.rs b/crates/wit-component/src/validation.rs
index c7bd5d33..1262add2 100644
--- a/crates/wit-component/src/validation.rs
+++ b/crates/wit-component/src/validation.rs
@@ -80,7 +80,7 @@ pub struct ValidatedModule<'a> {
     /// imported resources and this is only for exported resources. Exported
     /// resources still require intrinsics to be imported into the core module
     /// itself.
-    pub required_resource_funcs: IndexMap<String, IndexMap<&'a str, ResourceInfo<'a>>>,
+    pub required_resource_funcs: IndexMap<String, IndexMap<String, ResourceInfo>>,
 
     /// Whether or not this module exported a linear memory.
     pub has_memory: bool,
@@ -105,11 +105,11 @@ pub struct RequiredImports {
     pub resources: IndexSet<String>,
 }
 
-pub struct ResourceInfo<'a> {
-    pub drop_import: Option<&'a str>,
-    pub new_import: Option<&'a str>,
-    pub rep_import: Option<&'a str>,
-    pub dtor_export: Option<&'a str>,
+pub struct ResourceInfo {
+    pub drop_import: Option<String>,
+    pub new_import: Option<String>,
+    pub rep_import: Option<String>,
+    pub dtor_export: Option<String>,
     pub id: TypeId,
 }
 
@@ -255,7 +255,8 @@ pub fn validate_module<'a>(
             &metadata.resolve.name_world_key(name),
             &export_funcs,
             &types,
-            &mut ret,
+            &mut ret.post_returns,
+            &mut ret.required_resource_funcs,
         )?;
     }
 
@@ -269,7 +270,7 @@ pub fn validate_module<'a>(
                     name,
                     funcs,
                     &types,
-                    &mut ret,
+                    &mut ret.required_resource_funcs,
                 )?;
             }
             _ => bail!("import from `{name}` does not correspond to exported interface"),
@@ -285,7 +286,7 @@ fn validate_exported_interface_resource_imports<'a>(
     import_module: &str,
     funcs: &IndexMap<&'a str, u32>,
     types: &Types,
-    info: &mut ValidatedModule<'a>,
+    required_resource_funcs: &mut IndexMap<String, IndexMap<String, ResourceInfo>>,
 ) -> Result<()> {
     let is_resource = |name: &str| match resolve.interfaces[interface].types.get(name) {
         Some(ty) => matches!(resolve.types[*ty].kind, TypeDefKind::Resource),
@@ -295,17 +296,17 @@ fn validate_exported_interface_resource_imports<'a>(
         if valid_exported_resource_func(func_name, *ty, types, is_resource)?.is_none() {
             bail!("import of `{func_name}` is not a valid resource function");
         }
-        let info = info.required_resource_funcs.get_mut(import_module).unwrap();
+        let info = required_resource_funcs.get_mut(import_module).unwrap();
         if let Some(resource_name) = func_name.strip_prefix(RESOURCE_DROP) {
-            info[resource_name].drop_import = Some(func_name);
+            info[resource_name].drop_import = Some(func_name.to_string());
             continue;
         }
         if let Some(resource_name) = func_name.strip_prefix(RESOURCE_NEW) {
-            info[resource_name].new_import = Some(func_name);
+            info[resource_name].new_import = Some(func_name.to_string());
             continue;
         }
         if let Some(resource_name) = func_name.strip_prefix(RESOURCE_REP) {
-            info[resource_name].rep_import = Some(func_name);
+            info[resource_name].rep_import = Some(func_name.to_string());
             continue;
         }
 
@@ -324,6 +325,15 @@ pub struct ValidatedAdapter<'a> {
     /// imported interface.
     pub required_imports: IndexMap<String, RequiredImports>,
 
+    /// Resource-related functions required and imported which work over
+    /// exported resources from the final component.
+    ///
+    /// Note that this is disjoint from `required_imports` which handles
+    /// imported resources and this is only for exported resources. Exported
+    /// resources still require intrinsics to be imported into the core module
+    /// itself.
+    pub required_resource_funcs: IndexMap<String, IndexMap<String, ResourceInfo>>,
+
     /// This is the module and field name of the memory import, if one is
     /// specified.
     ///
@@ -373,7 +383,8 @@ pub fn validate_adapter_module<'a>(
     resolve: &'a Resolve,
     world: WorldId,
     metadata: &'a ModuleMetadata,
-    required: &IndexMap<String, (FuncType, Option<&Function>)>,
+    required_by_import: Option<&IndexMap<&str, FuncType>>,
+    exports: &IndexSet<WorldKey>,
     is_library: bool,
 ) -> Result<ValidatedAdapter<'a>> {
     let mut validator = Validator::new();
@@ -383,6 +394,7 @@ pub fn validate_adapter_module<'a>(
     let mut funcs = Vec::new();
     let mut ret = ValidatedAdapter {
         required_imports: Default::default(),
+        required_resource_funcs: Default::default(),
         needs_memory: None,
         needs_core_exports: Default::default(),
         import_realloc: None,
@@ -467,8 +479,9 @@ pub fn validate_adapter_module<'a>(
     }
 
     let types = types.unwrap();
-    for (name, funcs) in import_funcs {
-        if name == MAIN_MODULE_IMPORT_NAME {
+    let mut exported_resource_funcs = Vec::new();
+    for (name, funcs) in &import_funcs {
+        if *name == MAIN_MODULE_IMPORT_NAME {
             ret.needs_core_exports
                 .extend(funcs.iter().map(|(name, _ty)| name.to_string()));
             continue;
@@ -476,17 +489,22 @@ pub fn validate_adapter_module<'a>(
 
         // An empty module name is indicative of the top-level import namespace,
         // so look for top-level functions here.
-        if name == BARE_FUNC_MODULE_NAME {
-            let required = validate_imports_top_level(resolve, world, &funcs, &types)?;
+        if *name == BARE_FUNC_MODULE_NAME {
+            let required = validate_imports_top_level(resolve, world, funcs, &types)?;
             ret.required_imports
                 .insert(BARE_FUNC_MODULE_NAME.to_string(), required);
             continue;
         }
 
+        if let Some(interface_name) = name.strip_prefix("[export]") {
+            exported_resource_funcs.push((name, interface_name, &import_funcs[name]));
+            continue;
+        }
+
         match resolve.worlds[world].imports.get(&world_key(resolve, name)) {
             Some(WorldItem::Interface(interface)) => {
                 let required =
-                    validate_imported_interface(resolve, *interface, name, &funcs, &types)
+                    validate_imported_interface(resolve, *interface, name, funcs, &types)
                         .with_context(|| format!("failed to validate import interface `{name}`"))?;
                 let prev = ret.required_imports.insert(name.to_string(), required);
                 assert!(prev.is_none());
@@ -502,24 +520,46 @@ pub fn validate_adapter_module<'a>(
         }
     }
 
-    for (name, (ty, func)) in required {
-        let idx = match export_funcs.get(name.as_str()) {
-            Some(idx) => *idx,
-            None => bail!("adapter module did not export `{name}`"),
-        };
-        let id = types.function_at(idx);
-        let actual = types[id].unwrap_func();
-        validate_func_sig(name, ty, actual)?;
-
-        if let Some(func) = func {
-            let post_return = format!("{POST_RETURN_PREFIX}{name}");
-            if let Some(idx) = export_funcs.get(post_return.as_str()) {
-                let id = types.function_at(*idx);
-                let actual = types[id].unwrap_func();
-                validate_post_return(resolve, &actual, func)?;
-                let ok = ret.post_returns.insert(post_return);
-                assert!(ok);
+    if let Some(required) = required_by_import {
+        for (name, ty) in required {
+            let idx = match export_funcs.get(name) {
+                Some(idx) => *idx,
+                None => bail!("adapter module did not export `{name}`"),
+            };
+            let id = types.function_at(idx);
+            let actual = types[id].unwrap_func();
+            validate_func_sig(name, ty, actual)?;
+        }
+    }
+
+    let world = &resolve.worlds[world];
+
+    for name in exports {
+        validate_exported_item(
+            resolve,
+            &world.exports[name],
+            &resolve.name_world_key(name),
+            &export_funcs,
+            &types,
+            &mut ret.post_returns,
+            &mut ret.required_resource_funcs,
+        )?;
+    }
+
+    for (name, interface_name, funcs) in exported_resource_funcs {
+        let world_key = world_key(resolve, interface_name);
+        match world.exports.get(&world_key) {
+            Some(WorldItem::Interface(i)) => {
+                validate_exported_interface_resource_imports(
+                    resolve,
+                    *i,
+                    name,
+                    funcs,
+                    &types,
+                    &mut ret.required_resource_funcs,
+                )?;
             }
+            _ => bail!("import from `{name}` does not correspond to exported interface"),
         }
     }
 
@@ -722,9 +762,10 @@ fn validate_exported_item<'a>(
     resolve: &'a Resolve,
     item: &'a WorldItem,
     export_name: &str,
-    exports: &IndexMap<&'a str, u32>,
+    exports: &IndexMap<&str, u32>,
     types: &Types,
-    info: &mut ValidatedModule<'a>,
+    post_returns: &mut IndexSet<String>,
+    required_resource_funcs: &mut IndexMap<String, IndexMap<String, ResourceInfo>>,
 ) -> Result<()> {
     let mut validate = |func: &Function, name: Option<&str>| {
         let expected_export_name = func.core_export_name(name);
@@ -741,7 +782,7 @@ fn validate_exported_item<'a>(
 
         let post_return = format!("{POST_RETURN_PREFIX}{expected_export_name}");
         if let Some(index) = exports.get(&post_return[..]) {
-            let ok = info.post_returns.insert(post_return);
+            let ok = post_returns.insert(post_return);
             assert!(ok);
             let id = types.function_at(*index);
             let ty = types[id].unwrap_func();
@@ -776,14 +817,12 @@ fn validate_exported_item<'a>(
                     let ty = types[id].unwrap_func();
                     let expected = FuncType::new([ValType::I32], []);
                     validate_func_sig(name, &expected, ty)?;
-                    info.dtor_export = Some(name);
+                    info.dtor_export = Some(name.to_string());
                 }
-                let prev = map.insert(name.as_str(), info);
+                let prev = map.insert(name.to_string(), info);
                 assert!(prev.is_none());
             }
-            let prev = info
-                .required_resource_funcs
-                .insert(format!("[export]{export_name}"), map);
+            let prev = required_resource_funcs.insert(format!("[export]{export_name}"), map);
             assert!(prev.is_none());
         }
         // not required to have anything exported in the core wasm module


Test patch:
diff --git a/crates/wit-component/tests/components/link-resources/component.wat b/crates/wit-component/tests/components/link-resources/component.wat
new file mode 100644
index 00000000..8794c067
--- /dev/null
+++ b/crates/wit-component/tests/components/link-resources/component.wat
@@ -0,0 +1,196 @@
+(component
+  (core module (;0;)
+    (table (;0;) 0 funcref)
+    (memory (;0;) 17)
+    (global (;0;) (mut i32) i32.const 1048576)
+    (global (;1;) i32 i32.const 1048592)
+    (global (;2;) i32 i32.const 0)
+    (global (;3;) i32 i32.const 1048592)
+    (global (;4;) i32 i32.const 0)
+    (global (;5;) (mut i32) i32.const 1048592)
+    (global (;6;) (mut i32) i32.const 1114112)
+    (export "__stack_pointer" (global 0))
+    (export "bar:memory_base" (global 1))
+    (export "bar:table_base" (global 2))
+    (export "foo:memory_base" (global 3))
+    (export "foo:table_base" (global 4))
+    (export "__heap_base" (global 5))
+    (export "__heap_end" (global 6))
+    (export "__indirect_function_table" (table 0))
+    (export "memory" (memory 0))
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core module (;1;)
+    (@dylink.0
+      (mem-info (memory 0 4))
+    )
+    (type (;0;) (func (param i32) (result i32)))
+    (func $foo (;0;) (type 0) (param i32) (result i32)
+      unreachable
+    )
+    (func $x_ctor (;1;) (type 0) (param i32) (result i32)
+      unreachable
+    )
+    (func $x_get (;2;) (type 0) (param i32) (result i32)
+      unreachable
+    )
+    (export "test:test/foo#[constructor]x" (func $x_ctor))
+    (export "test:test/foo#[method]x.get" (func $x_get))
+  )
+  (core module (;2;)
+    (@dylink.0
+      (mem-info (memory 0 4))
+      (needed "foo")
+    )
+    (type (;0;) (func (param i32) (result i32)))
+    (func $x_ctor (;0;) (type 0) (param i32) (result i32)
+      unreachable
+    )
+    (func $x_get (;1;) (type 0) (param i32) (result i32)
+      unreachable
+    )
+    (export "test:test/bar#[constructor]x" (func $x_ctor))
+    (export "test:test/bar#[method]x.get" (func $x_get))
+  )
+  (core module (;3;)
+    (type (;0;) (func))
+    (type (;1;) (func (param i32)))
+    (import "env" "memory" (memory (;0;) 0))
+    (import "env" "__indirect_function_table" (table (;0;) 0 funcref))
+    (func (;0;) (type 0))
+    (start 0)
+    (elem (;0;) (i32.const 0) func)
+    (elem (;1;) (i32.const 0) func)
+    (data (;0;) (i32.const 1048576) "\00\00\00\00\00\00\10\00")
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core instance (;0;) (instantiate 0))
+  (alias core export 0 "memory" (core memory (;0;)))
+  (alias core export 0 "__heap_base" (core global (;0;)))
+  (alias core export 0 "__heap_end" (core global (;1;)))
+  (core instance (;1;)
+    (export "__heap_base" (global 0))
+    (export "__heap_end" (global 1))
+  )
+  (core instance (;2;))
+  (alias core export 0 "memory" (core memory (;1;)))
+  (alias core export 0 "__indirect_function_table" (core table (;0;)))
+  (alias core export 0 "__stack_pointer" (core global (;2;)))
+  (alias core export 0 "foo:memory_base" (core global (;3;)))
+  (alias core export 0 "foo:table_base" (core global (;4;)))
+  (core instance (;3;)
+    (export "memory" (memory 1))
+    (export "__indirect_function_table" (table 0))
+    (export "__stack_pointer" (global 2))
+    (export "__memory_base" (global 3))
+    (export "__table_base" (global 4))
+  )
+  (type (;0;) (resource (rep i32)))
+  (core instance (;4;) (instantiate 1
+      (with "GOT.mem" (instance 1))
+      (with "GOT.func" (instance 2))
+      (with "env" (instance 3))
+    )
+  )
+  (alias core export 0 "__heap_base" (core global (;5;)))
+  (alias core export 0 "__heap_end" (core global (;6;)))
+  (core instance (;5;)
+    (export "__heap_base" (global 5))
+    (export "__heap_end" (global 6))
+  )
+  (core instance (;6;))
+  (alias core export 0 "memory" (core memory (;2;)))
+  (alias core export 0 "__indirect_function_table" (core table (;1;)))
+  (alias core export 0 "__stack_pointer" (core global (;7;)))
+  (alias core export 0 "bar:memory_base" (core global (;8;)))
+  (alias core export 0 "bar:table_base" (core global (;9;)))
+  (core instance (;7;)
+    (export "memory" (memory 2))
+    (export "__indirect_function_table" (table 1))
+    (export "__stack_pointer" (global 7))
+    (export "__memory_base" (global 8))
+    (export "__table_base" (global 9))
+  )
+  (type (;1;) (resource (rep i32)))
+  (core instance (;8;) (instantiate 2
+      (with "GOT.mem" (instance 5))
+      (with "GOT.func" (instance 6))
+      (with "env" (instance 7))
+    )
+  )
+  (core instance (;9;) (instantiate 3
+      (with "env" (instance 0))
+      (with "bar" (instance 8))
+      (with "foo" (instance 4))
+    )
+  )
+  (type (;2;) (own 0))
+  (type (;3;) (func (param "v" u32) (result 2)))
+  (alias core export 4 "test:test/foo#[constructor]x" (core func (;0;)))
+  (func (;0;) (type 3) (canon lift (core func 0)))
+  (type (;4;) (borrow 0))
+  (type (;5;) (func (param "self" 4) (result u32)))
+  (alias core export 4 "test:test/foo#[method]x.get" (core func (;1;)))
+  (func (;1;) (type 5) (canon lift (core func 1)))
+  (component (;0;)
+    (import "import-type-x" (type (;0;) (sub resource)))
+    (type (;1;) (own 0))
+    (type (;2;) (func (param "v" u32) (result 1)))
+    (import "import-constructor-x" (func (;0;) (type 2)))
+    (type (;3;) (borrow 0))
+    (type (;4;) (func (param "self" 3) (result u32)))
+    (import "import-method-x-get" (func (;1;) (type 4)))
+    (export (;5;) "x" (type 0))
+    (type (;6;) (own 5))
+    (type (;7;) (func (param "v" u32) (result 6)))
+    (export (;2;) "[constructor]x" (func 0) (func (type 7)))
+    (type (;8;) (borrow 5))
+    (type (;9;) (func (param "self" 8) (result u32)))
+    (export (;3;) "[method]x.get" (func 1) (func (type 9)))
+  )
+  (instance (;0;) (instantiate 0
+      (with "import-constructor-x" (func 0))
+      (with "import-method-x-get" (func 1))
+      (with "import-type-x" (type 0))
+    )
+  )
+  (export (;1;) (interface "test:test/foo") (instance 0))
+  (type (;6;) (own 1))
+  (type (;7;) (func (param "v" u32) (result 6)))
+  (alias core export 8 "test:test/bar#[constructor]x" (core func (;2;)))
+  (func (;2;) (type 7) (canon lift (core func 2)))
+  (type (;8;) (borrow 1))
+  (type (;9;) (func (param "self" 8) (result u32)))
+  (alias core export 8 "test:test/bar#[method]x.get" (core func (;3;)))
+  (func (;3;) (type 9) (canon lift (core func 3)))
+  (component (;1;)
+    (import "import-type-x" (type (;0;) (sub resource)))
+    (type (;1;) (own 0))
+    (type (;2;) (func (param "v" u32) (result 1)))
+    (import "import-constructor-x" (func (;0;) (type 2)))
+    (type (;3;) (borrow 0))
+    (type (;4;) (func (param "self" 3) (result u32)))
+    (import "import-method-x-get" (func (;1;) (type 4)))
+    (export (;5;) "x" (type 0))
+    (type (;6;) (own 5))
+    (type (;7;) (func (param "v" u32) (result 6)))
+    (export (;2;) "[constructor]x" (func 0) (func (type 7)))
+    (type (;8;) (borrow 5))
+    (type (;9;) (func (param "self" 8) (result u32)))
+    (export (;3;) "[method]x.get" (func 1) (func (type 9)))
+  )
+  (instance (;2;) (instantiate 1
+      (with "import-constructor-x" (func 2))
+      (with "import-method-x-get" (func 3))
+      (with "import-type-x" (type 1))
+    )
+  )
+  (export (;3;) (interface "test:test/bar") (instance 2))
+  (@producers
+    (processed-by "wit-component" "$CARGO_PKG_VERSION")
+  )
+)
\ No newline at end of file
diff --git a/crates/wit-component/tests/components/link-resources/component.wit.print b/crates/wit-component/tests/components/link-resources/component.wit.print
new file mode 100644
index 00000000..273010a4
--- /dev/null
+++ b/crates/wit-component/tests/components/link-resources/component.wit.print
@@ -0,0 +1,6 @@
+package root:component
+
+world root {
+  export test:test/foo
+  export test:test/bar
+}
diff --git a/crates/wit-component/tests/components/link-resources/lib-bar.wat b/crates/wit-component/tests/components/link-resources/lib-bar.wat
new file mode 100644
index 00000000..53ce67f6
--- /dev/null
+++ b/crates/wit-component/tests/components/link-resources/lib-bar.wat
@@ -0,0 +1,15 @@
+(module
+  (@dylink.0
+    (mem-info (memory 0 4))
+    (needed "foo")
+  )
+  (type (func (param i32) (result i32)))
+  (func $x_ctor (type 0) (param i32) (result i32)
+    unreachable
+  )
+  (func $x_get (type 0) (param i32) (result i32)
+    unreachable
+  )
+  (export "test:test/bar#[constructor]x" (func $x_ctor))
+  (export "test:test/bar#[method]x.get" (func $x_get))
+)
diff --git a/crates/wit-component/tests/components/link-resources/lib-bar.wit b/crates/wit-component/tests/components/link-resources/lib-bar.wit
new file mode 100644
index 00000000..3fae33dd
--- /dev/null
+++ b/crates/wit-component/tests/components/link-resources/lib-bar.wit
@@ -0,0 +1,13 @@
+package test:test;
+
+interface bar {
+  resource x {
+    constructor(v: u32);
+    get: func() -> u32;
+  }
+}
+
+world lib-bar {
+    import bar;
+    export bar;
+}
diff --git a/crates/wit-component/tests/components/link-resources/lib-foo.wat b/crates/wit-component/tests/components/link-resources/lib-foo.wat
new file mode 100644
index 00000000..f561b00e
--- /dev/null
+++ b/crates/wit-component/tests/components/link-resources/lib-foo.wat
@@ -0,0 +1,17 @@
+(module
+  (@dylink.0
+    (mem-info (memory 0 4))
+  )
+  (type (func (param i32) (result i32)))
+  (func $foo (type 0) (param i32) (result i32)
+    unreachable
+  )
+  (func $x_ctor (type 0) (param i32) (result i32)
+    unreachable
+  )
+  (func $x_get (type 0) (param i32) (result i32)
+    unreachable
+  )
+  (export "test:test/foo#[constructor]x" (func $x_ctor))
+  (export "test:test/foo#[method]x.get" (func $x_get))
+)
diff --git a/crates/wit-component/tests/components/link-resources/lib-foo.wit b/crates/wit-component/tests/components/link-resources/lib-foo.wit
new file mode 100644
index 00000000..47bd48c1
--- /dev/null
+++ b/crates/wit-component/tests/components/link-resources/lib-foo.wit
@@ -0,0 +1,13 @@
+package test:test;
+
+interface foo {
+  resource x {
+    constructor(v: u32);
+    get: func() -> u32;
+  }
+}
+
+world lib-foo {
+  import foo;
+  export foo;
+}
diff --git a/tests/local/exnref/exnref.wast b/tests/local/exnref/exnref.wast
new file mode 100644
index 00000000..421eca77
--- /dev/null
+++ b/tests/local/exnref/exnref.wast
@@ -0,0 +1,5 @@
+(module
+  (func (param exnref))
+  (func (param (ref null exn)))
+  (func (param (ref exn)))
+)
diff --git a/tests/local/exnref/throw_ref.wast b/tests/local/exnref/throw_ref.wast
new file mode 100644
index 00000000..67ead819
--- /dev/null
+++ b/tests/local/exnref/throw_ref.wast
@@ -0,0 +1,6 @@
+(module
+  (func (param exnref)
+    local.get 0
+    throw_ref
+  )
+)
diff --git a/tests/local/exnref/try_table.wast b/tests/local/exnref/try_table.wast
new file mode 100644
index 00000000..ade6a0a5
--- /dev/null
+++ b/tests/local/exnref/try_table.wast
@@ -0,0 +1,95 @@
+(module
+  (tag $a (param i32))
+
+  (func
+    (; empty try_table ;)
+    try_table
+    end
+
+    (; try_table with result  ;)
+    try_table (result i32)
+      i32.const 0
+    end
+    drop
+
+    (; try_table can catches ;)
+    try_table (catch $a 0)
+    end
+
+    try_table (catch $a 0) (catch $a 0)
+    end
+
+    try_table (catch_all 0)
+    end
+
+    try_table (catch $a 0) (catch_all 0)
+    end
+
+    try_table (catch $a 0) (catch $a 0) (catch_all 0)
+    end
+
+    (; try_table can have results and catches ;)
+    try_table (result i32) (catch $a 0)
+      i32.const 0
+    end
+    drop
+
+    try_table (result i32) (catch $a 0) (catch $a 0)
+      i32.const 0
+    end
+    drop
+
+    try_table (result i32) (catch_all 0)
+      i32.const 0
+    end
+    drop
+
+    try_table (result i32) (catch $a 0) (catch_all 0)
+      i32.const 0
+    end
+    drop
+
+    try_table (result i32) (catch $a 0) (catch $a 0) (catch_all 0)
+      i32.const 0
+    end
+    drop
+
+    (; mixes of catch, catch_ref, catch_all, and catch_all_ref ;)
+
+    try_table (catch_ref $a 0)
+    end
+
+    try_table (catch_ref $a 0) (catch_ref $a 0)
+    end
+
+    try_table (catch $a 0) (catch_ref $a 0)
+    end
+
+    try_table (catch_ref $a 0) (catch_ref $a 0)
+    end
+
+    try_table (catch_ref $a 0) (catch $a 0)
+    end
+
+    try_table (catch_all 0)
+    end
+
+    try_table (catch_all_ref 0)
+    end
+
+    try_table (catch_ref $a 0) (catch_all_ref 0)
+    end
+
+    try_table (catch_ref $a 0) (catch_ref $a 0) (catch_all_ref 0)
+    end
+
+    try_table (catch $a 0) (catch_ref $a 0) (catch_all_ref 0)
+    end
+
+    try_table (catch_ref $a 0) (catch_ref $a 0) (catch_all_ref 0)
+    end
+
+    try_table (catch_ref $a 0) (catch $a 0) (catch_all_ref 0)
+    end
+  )
+)
diff --git a/tests/roundtrip.rs b/tests/roundtrip.rs
index ecaf9a7d..6d5eac7b 100644
--- a/tests/roundtrip.rs
+++ b/tests/roundtrip.rs
@@ -193,6 +193,9 @@ fn skip_validation(test: &Path) -> bool {
         "/proposals/gc/type-equivalence.wast",
         "/proposals/gc/type-rec.wast",
         "/proposals/gc/type-subtyping.wast",
+        "/exnref/exnref.wast",
+        "/exnref/throw_ref.wast",
+        "/exnref/try_table.wast",
     ];
     let test_path = test.to_str().unwrap().replace("\\", "/"); // for windows paths
     if broken.iter().any(|x| test_path.contains(x)) {