Repository: bytecodealliance/wasmtime
Pull request #8172 (2024-03-18 20:23:33)

 Skip type checks on tables that don't need it 
This commit implements an optimization to skip type checks in
`call_indirect` for tables that don't require it. With the
function-references proposal it's possible to have tables of a single
type of function as opposed to today's default `funcref` which is a
heterogenous set of functions. In this situation it's possible that a
`call_indirect`'s type tag matches the type tag of a
`table`-of-typed-`funcref`-values, meaning that it's impossible for the
type check to fail.

The type check of a function pointer in `call_indirect` is refactored
here to take the table's type into account. Various things are shuffled
around to ensure that the right traps still show up in the right places
but the important part is that, when possible, the type check is omitted
entirely.


Patch:
diff --git a/RELEASES.md b/RELEASES.md
index 30e58bfdc..9a554aed0 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -8,6 +8,10 @@ Unreleased.
 
 ### Changed
 
+* The `-S common` flag is renamed to `-S cli`, to better reflect that it provides
+  the wasi-cli APIs. `-S common` is still accepted for now, and will be deprecated
+  in the future.
+
 --------------------------------------------------------------------------------
 
 ## 19.0.0
diff --git a/cranelift/codegen/src/isa/x64/lower.isle b/cranelift/codegen/src/isa/x64/lower.isle
index 9bb01775e..31cc17518 100644
--- a/cranelift/codegen/src/isa/x64/lower.isle
+++ b/cranelift/codegen/src/isa/x64/lower.isle
@@ -1485,20 +1485,12 @@
 
 ;;;; Rules for `insertlane` ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-(rule (lower (insertlane vec @ (value_type ty) val (u8_from_uimm8 idx)))
-      (vec_insert_lane ty vec val idx))
-
-;; Helper function used below for `insertlane` but also here for other
-;; lowerings.
-;;
-;; Note that the `Type` used here is the type of vector the insertion is
-;; happening into, or the type of the first `Reg` argument.
-(decl vec_insert_lane (Type Xmm RegMem u8) Xmm)
-
-;; i8x16.replace_lane
-(rule 1 (vec_insert_lane $I8X16 vec val idx)
-        (if-let $true (use_sse41))
-        (x64_pinsrb vec val idx))
+(rule 1 (lower (insertlane vec @ (value_type $I8X16) val (u8_from_uimm8 idx)))
+  (if-let $true (use_sse41))
+  (x64_pinsrb vec val idx))
+(rule 2 (lower (insertlane vec @ (value_type $I8X16) (sinkable_load_exact val) (u8_from_uimm8 idx)))
+  (if-let $true (use_sse41))
+  (x64_pinsrb vec val idx))
 
 ;; This lowering is particularly unoptimized and is mostly just here to work
 ;; rather than here to be fast. Requiring SSE 4.1 for the above lowering isn't
@@ -1524,7 +1516,7 @@
 ;; This all, laboriously, gets the `val` into the desired lane so it's then
 ;; `por`'d with the original vec-with-a-hole to produce the final result of the
 ;; insertion.
-(rule (vec_insert_lane $I8X16 vec val n)
+(rule (lower (insertlane vec @ (value_type $I8X16) val (u8_from_uimm8 n)))
       (let ((vec_with_hole Xmm (x64_pand vec (insert_i8x16_lane_hole n)))
             (val Gpr (x64_movzx (ExtMode.BL) val))
             (val Gpr (x64_shl $I32 val (Imm8Reg.Imm8 (u8_shl (u8_and n 3) 3))))
@@ -1540,102 +1532,108 @@
 (rule (insert_i8x16_lane_pshufd_imm 2) 0b01_00_01_01)
 (rule (insert_i8x16_lane_pshufd_imm 3) 0b00_01_01_01)
 
+
 ;; i16x8.replace_lane
-(rule (vec_insert_lane $I16X8 vec val idx)
-      (x64_pinsrw vec val idx))
+(rule (lower (insertlane vec @ (value_type $I16X8) val (u8_from_uimm8 idx)))
+  (x64_pinsrw vec val idx))
+(rule 1 (lower (insertlane vec @ (value_type $I16X8) (sinkable_load_exact val) (u8_from_uimm8 idx)))
+  (x64_pinsrw vec val idx))
 
 ;; i32x4.replace_lane
-(rule 1 (vec_insert_lane $I32X4 vec val idx)
+(rule 1 (lower (insertlane vec @ (value_type $I32X4) val (u8_from_uimm8 idx)))
         (if-let $true (use_sse41))
         (x64_pinsrd vec val idx))
 
-(rule (vec_insert_lane $I32X4 vec val 0)
-      (x64_movss_regmove vec (x64_movd_to_xmm val)))
+(rule (lower (insertlane vec @ (value_type $I32X4) val (u8_from_uimm8 0)))
+  (x64_movss_regmove vec (x64_movd_to_xmm val)))
 
 ;; tmp    = [ vec[1] vec[0] val[1] val[0] ]
 ;; result = [ vec[3] vec[2] tmp[0] tmp[2] ]
-(rule (vec_insert_lane $I32X4 vec val 1)
+(rule (lower (insertlane vec @ (value_type $I32X4) val (u8_from_uimm8 1)))
       (let ((val Xmm (x64_movd_to_xmm val))
             (vec Xmm vec))
         (x64_shufps (x64_punpcklqdq val vec) vec 0b11_10_00_10)))
 
 ;; tmp    = [ vec[0] vec[3] val[0] val[0] ]
 ;; result = [ tmp[2] tmp[0] vec[1] vec[0] ]
-(rule (vec_insert_lane $I32X4 vec val 2)
+(rule (lower (insertlane vec @ (value_type $I32X4) val (u8_from_uimm8 2)))
       (let ((val Xmm (x64_movd_to_xmm val))
             (vec Xmm vec))
         (x64_shufps vec (x64_shufps val vec 0b00_11_00_00) 0b10_00_01_00)))
 
 ;; tmp    = [ vec[3] vec[2] val[1] val[0] ]
 ;; result = [ tmp[0] tmp[2] vec[1] vec[0] ]
-(rule (vec_insert_lane $I32X4 vec val 3)
+(rule (lower (insertlane vec @ (value_type $I32X4) val (u8_from_uimm8 3)))
       (let ((val Xmm (x64_movd_to_xmm val))
             (vec Xmm vec))
         (x64_shufps vec (x64_shufps val vec 0b11_10_01_00) 0b00_10_01_00)))
 
 ;; i64x2.replace_lane
-(rule 1 (vec_insert_lane $I64X2 vec val idx)
+(rule 1 (lower (insertlane vec @ (value_type $I64X2) val (u8_from_uimm8 idx)))
         (if-let $true (use_sse41))
         (x64_pinsrq vec val idx))
-(rule (vec_insert_lane $I64X2 vec val 0)
+(rule (lower (insertlane vec @ (value_type $I64X2) val (u8_from_uimm8 0)))
       (x64_movsd_regmove vec (x64_movq_to_xmm val)))
-(rule (vec_insert_lane $I64X2 vec val 1)
+(rule (lower (insertlane vec @ (value_type $I64X2) val (u8_from_uimm8 1)))
       (x64_punpcklqdq vec (x64_movq_to_xmm val)))
 
+(rule 1 (lower (insertlane vec @ (value_type $F32X4) (sinkable_load val) (u8_from_uimm8 idx)))
+  (if-let $true (use_sse41))
+  (x64_insertps vec val (sse_insertps_lane_imm idx)))
+(rule (lower (insertlane vec @ (value_type $F32X4) val (u8_from_uimm8 idx)))
+  (f32x4_insertlane vec val idx))
+
+;; Helper function used below for `insertlane` but also here for other
+(decl f32x4_insertlane (Xmm Xmm u8) Xmm)
+
 ;; f32x4.replace_lane
-(rule 1 (vec_insert_lane $F32X4 vec val idx)
+(rule 1 (f32x4_insertlane vec val idx)
         (if-let $true (use_sse41))
         (x64_insertps vec val (sse_insertps_lane_imm idx)))
 
-;; f32x4.replace_lane 0 - without insertps
-(rule (vec_insert_lane $F32X4 vec (RegMem.Reg val) 0)
+;; External rust code used to calculate the immediate value to `insertps`.
+(decl sse_insertps_lane_imm (u8) u8)
+(extern constructor sse_insertps_lane_imm sse_insertps_lane_imm)
+
+;; f32x4.replace_lane 0
+(rule (f32x4_insertlane vec val 0)
       (x64_movss_regmove vec val))
 
-;; f32x4.replace_lane 1 - without insertps
+;; f32x4.replace_lane 1
 ;; tmp    = [ vec[1] vec[0] val[1] val[0] ]
 ;; result = [ vec[3] vec[2] tmp[0] tmp[2] ]
-(rule (vec_insert_lane $F32X4 vec (RegMem.Reg val) 1)
+(rule (f32x4_insertlane vec val 1)
       (let ((tmp Xmm (x64_movlhps val vec)))
         (x64_shufps tmp vec 0b11_10_00_10)))
 
-;; f32x4.replace_lane 2 - without insertps
+;; f32x4.replace_lane 2
 ;; tmp    = [ vec[0] vec[3] val[0] val[0] ]
 ;; result = [ tmp[2] tmp[0] vec[1] vec[0] ]
-(rule (vec_insert_lane $F32X4 vec (RegMem.Reg val) 2)
+(rule (f32x4_insertlane vec val 2)
       (let ((tmp Xmm (x64_shufps val vec 0b00_11_00_00)))
         (x64_shufps vec tmp 0b10_00_01_00)))
 
-;; f32x4.replace_lane 3 - without insertps
+;; f32x4.replace_lane 3
 ;; tmp    = [ vec[3] vec[2] val[1] val[0] ]
 ;; result = [ tmp[0] tmp[2] vec[1] vec[0] ]
-(rule (vec_insert_lane $F32X4 vec (RegMem.Reg val) 3)
+(rule (f32x4_insertlane vec val 3)
       (let ((tmp Xmm (x64_shufps val vec 0b11_10_01_00)))
         (x64_shufps vec tmp 0b00_10_01_00)))
 
-;; Recursively delegate to the above rules by loading from memory first.
-(rule (vec_insert_lane $F32X4 vec (RegMem.Mem addr) idx)
-      (vec_insert_lane $F32X4 vec (x64_movss_load addr) idx))
-
-;; External rust code used to calculate the immediate value to `insertps`.
-(decl sse_insertps_lane_imm (u8) u8)
-(extern constructor sse_insertps_lane_imm sse_insertps_lane_imm)
-
 ;; f64x2.replace_lane 0
 ;;
 ;; Here the `movsd` instruction is used specifically to specialize moving
 ;; into the fist lane where unlike above cases we're not using the lane
 ;; immediate as an immediate to the instruction itself.
-(rule (vec_insert_lane $F64X2 vec (RegMem.Reg val) 0)
+(rule (lower (insertlane vec @ (value_type $F64X2) val (u8_from_uimm8 0)))
       (x64_movsd_regmove vec val))
-(rule (vec_insert_lane $F64X2 vec (RegMem.Mem val) 0)
-      (x64_movsd_regmove vec (x64_movsd_load val)))
 
 ;; f64x2.replace_lane 1
 ;;
 ;; Here the `movlhps` instruction is used specifically to specialize moving
 ;; into the second lane where unlike above cases we're not using the lane
 ;; immediate as an immediate to the instruction itself.
-(rule (vec_insert_lane $F64X2 vec val 1)
+(rule (lower (insertlane vec @ (value_type $F64X2) val (u8_from_uimm8 1)))
       (x64_movlhps vec val))
 
 ;;;; Rules for `smin`, `smax`, `umin`, `umax` ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -2804,9 +2802,9 @@
             (x64_pshufd y 3)
             (x64_pshufd z 3)))
 
-          (tmp Xmm (vec_insert_lane $F32X4 x0 x1 1))
-          (tmp Xmm (vec_insert_lane $F32X4 tmp x2 2))
-          (tmp Xmm (vec_insert_lane $F32X4 tmp x3 3))
+          (tmp Xmm (f32x4_insertlane x0 x1 1))
+          (tmp Xmm (f32x4_insertlane tmp x2 2))
+          (tmp Xmm (f32x4_insertlane tmp x3 3))
         )
         tmp))
 (rule (lower (has_type $F64X2 (fma x y z)))
@@ -2820,7 +2818,7 @@
             (x64_pshufd y 0xee)
             (x64_pshufd z 0xee)))
         )
-        (vec_insert_lane $F64X2 x0 x1 1)))
+        (x64_movlhps x0 x1)))
 
 
 ;; Special case for when the `fma` feature is active and a native instruction
@@ -4030,11 +4028,11 @@
           (libcall LibCall (round_libcall $F32 imm))
           (result Xmm (libcall_1 libcall a))
           (a1 Xmm (libcall_1 libcall (x64_pshufd a 1)))
-          (result Xmm (vec_insert_lane $F32X4 result a1 1))
+          (result Xmm (f32x4_insertlane result a1 1))
           (a2 Xmm (libcall_1 libcall (x64_pshufd a 2)))
-          (result Xmm (vec_insert_lane $F32X4 result a2 2))
+          (result Xmm (f32x4_insertlane result a2 2))
           (a3 Xmm (libcall_1 libcall (x64_pshufd a 3)))
-          (result Xmm (vec_insert_lane $F32X4 result a3 3))
+          (result Xmm (f32x4_insertlane result a3 3))
         )
         result))
 (rule (x64_round $F64X2 (RegMem.Reg a) imm)
@@ -4042,9 +4040,8 @@
           (libcall LibCall (round_libcall $F64 imm))
           (result Xmm (libcall_1 libcall a))
           (a1 Xmm (libcall_1 libcall (x64_pshufd a 0b00_00_11_10)))
-          (result Xmm (vec_insert_lane $F64X2 result a1 1))
         )
-        result))
+        (x64_movlhps result a1)))
 (rule (x64_round ty (RegMem.Mem addr) imm)
       (x64_round ty (RegMem.Reg (x64_load ty addr (ExtKind.ZeroExtend))) imm))
 
diff --git a/cranelift/wasm/src/code_translator.rs b/cranelift/wasm/src/code_translator.rs
index bb3d6c21f..adbe7ea65 100644
--- a/cranelift/wasm/src/code_translator.rs
+++ b/cranelift/wasm/src/code_translator.rs
@@ -659,6 +659,13 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
                 callee,
                 state.peekn(num_args),
             )?;
+            let call = match call {
+                Some(call) => call,
+                None => {
+                    state.reachable = false;
+                    return Ok(());
+                }
+            };
             let inst_results = builder.inst_results(call);
             debug_assert_eq!(
                 inst_results.len(),
diff --git a/cranelift/wasm/src/environ/dummy.rs b/cranelift/wasm/src/environ/dummy.rs
index dac12cb1b..78ee7faf4 100644
--- a/cranelift/wasm/src/environ/dummy.rs
+++ b/cranelift/wasm/src/environ/dummy.rs
@@ -349,7 +349,7 @@ impl<'dummy_environment> FuncEnvironment for DummyFuncEnvironment<'dummy_environ
         sig_ref: ir::SigRef,
         callee: ir::Value,
         call_args: &[ir::Value],
-    ) -> WasmResult<ir::Inst> {
+    ) -> WasmResult<Option<ir::Inst>> {
         // Pass the current function's vmctx parameter on to the callee.
         let vmctx = builder
             .func
@@ -376,10 +376,12 @@ impl<'dummy_environment> FuncEnvironment for DummyFuncEnvironment<'dummy_environ
         args.extend(call_args.iter().cloned(), &mut builder.func.dfg.value_lists);
         args.push(vmctx, &mut builder.func.dfg.value_lists);
 
-        Ok(builder
-            .ins()
-            .CallIndirect(ir::Opcode::CallIndirect, INVALID, sig_ref, args)
-            .0)
+        Ok(Some(
+            builder
+                .ins()
+                .CallIndirect(ir::Opcode::CallIndirect, INVALID, sig_ref, args)
+                .0,
+        ))
     }
 
     fn translate_return_call_indirect(
diff --git a/cranelift/wasm/src/environ/spec.rs b/cranelift/wasm/src/environ/spec.rs
index 9d13f4e5a..d99696270 100644
--- a/cranelift/wasm/src/environ/spec.rs
+++ b/cranelift/wasm/src/environ/spec.rs
@@ -193,6 +193,8 @@ pub trait FuncEnvironment: TargetEnvironment {
     /// The signature `sig_ref` was previously created by `make_indirect_sig()`.
     ///
     /// Return the call instruction whose results are the WebAssembly return values.
+    /// Returns `None` if this statically traps instead of creating a call
+    /// instruction.
     fn translate_call_indirect(
         &mut self,
         builder: &mut FunctionBuilder,
@@ -201,7 +203,7 @@ pub trait FuncEnvironment: TargetEnvironment {
         sig_ref: ir::SigRef,
         callee: ir::Value,
         call_args: &[ir::Value],
-    ) -> WasmResult<ir::Inst>;
+    ) -> WasmResult<Option<ir::Inst>>;
 
     /// Translate a `return_call` WebAssembly instruction at the builder's
     /// current position.
diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs
index 5875e0b8d..d76dc9b23 100644
--- a/crates/cli-flags/src/lib.rs
+++ b/crates/cli-flags/src/lib.rs
@@ -252,7 +252,9 @@ wasmtime_option_group! {
 wasmtime_option_group! {
     #[derive(PartialEq, Clone)]
     pub struct WasiOptions {
-        /// Enable support for WASI common APIs
+        /// Enable support for WASI CLI APIs, including filesystems, sockets, clocks, and random.
+        pub cli: Option<bool>,
+        /// Deprecated alias for `cli`
         pub common: Option<bool>,
         /// Enable suport for WASI neural network API (experimental)
         pub nn: Option<bool>,
@@ -265,7 +267,7 @@ wasmtime_option_group! {
         pub listenfd: Option<bool>,
         /// Grant access to the given TCP listen socket
         pub tcplisten: Vec<String>,
-        /// Implement WASI with preview2 primitives (experimental).
+        /// Implement WASI CLI APIs with preview2 primitives (experimental).
         ///
         /// Indicates that the implementation of WASI preview1 should be backed by
         /// the preview2 implementation for components.
diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs
index a4d8c5344..d46b70cd6 100644
--- a/crates/cranelift/src/func_environ.rs
+++ b/crates/cranelift/src/func_environ.rs
@@ -13,15 +13,15 @@ use cranelift_entity::{EntityRef, PrimaryMap, SecondaryMap};
 use cranelift_frontend::FunctionBuilder;
 use cranelift_frontend::Variable;
 use cranelift_wasm::{
-    FuncIndex, FuncTranslationState, GlobalIndex, GlobalVariable, Heap, HeapData, HeapStyle,
-    MemoryIndex, TableData, TableIndex, TableSize, TargetEnvironment, TypeIndex, WasmHeapType,
-    WasmRefType, WasmResult, WasmValType,
+    EngineOrModuleTypeIndex, FuncIndex, FuncTranslationState, GlobalIndex, GlobalVariable, Heap,
+    HeapData, HeapStyle, MemoryIndex, TableData, TableIndex, TableSize, TargetEnvironment,
+    TypeIndex, WasmHeapType, WasmRefType, WasmResult, WasmValType,
 };
 use std::mem;
 use wasmparser::Operator;
 use wasmtime_environ::{
-    BuiltinFunctionIndex, MemoryPlan, MemoryStyle, Module, ModuleTranslation, ModuleTypesBuilder,
-    PtrSize, TableStyle, Tunables, TypeConvert, VMOffsets, WASM_PAGE_SIZE,
+    BuiltinFunctionIndex, MemoryPlan, MemoryStyle, Module, ModuleTranslation, ModuleType,
+    ModuleTypesBuilder, PtrSize, TableStyle, Tunables, TypeConvert, VMOffsets, WASM_PAGE_SIZE,
 };
 use wasmtime_environ::{FUNCREF_INIT_BIT, FUNCREF_MASK};
 
@@ -1046,6 +1046,16 @@ struct Call<'a, 'func, 'module_env> {
     tail: bool,
 }
 
+enum CheckIndirectCallTypeSignature {
+    Runtime,
+    StaticMatch {
+        /// Whether or not the funcref may be null or if it's statically known
+        /// to not be null.
+        may_be_null: bool,
+    },
+    StaticTrap,
+}
+
 impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> {
     /// Create a new `Call` site that will do regular, non-tail calls.
     pub fn new(
@@ -1147,67 +1157,160 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> {
         sig_ref: ir::SigRef,
         callee: ir::Value,
         call_args: &[ir::Value],
-    ) -> WasmResult<ir::Inst> {
-        let pointer_type = self.env.pointer_type();
-
+    ) -> WasmResult<Option<ir::Inst>> {
         // Get the funcref pointer from the table.
         let funcref_ptr =
             self.env
                 .get_or_init_func_ref_table_elem(self.builder, table_index, callee);
 
         // If necessary, check the signature.
-        match self.env.module.table_plans[table_index].style {
-            TableStyle::CallerChecksSignature => {
-                let sig_id_size = self.env.offsets.size_of_vmshared_type_index();
-                let sig_id_type = Type::int(u16::from(sig_id_size) * 8).unwrap();
-                let vmctx = self.env.vmctx(self.builder.func);
-                let base = self.builder.ins().global_value(pointer_type, vmctx);
-
-                // Load the caller ID. This requires loading the `*mut
-                // VMFuncRef` base pointer from `VMContext` and then loading,
-                // based on `SignatureIndex`, the corresponding entry.
-                let mem_flags = ir::MemFlags::trusted().with_readonly();
-                let signatures = self.builder.ins().load(
-                    pointer_type,
-                    mem_flags,
-                    base,
-                    i32::try_from(self.env.offsets.vmctx_type_ids_array()).unwrap(),
-                );
-                let sig_index = self.env.module.types[ty_index].unwrap_function();
-                let offset =
-                    i32::try_from(sig_index.as_u32().checked_mul(sig_id_type.bytes()).unwrap())
-                        .unwrap();
-                let caller_sig_id =
-                    self.builder
-                        .ins()
-                        .load(sig_id_type, mem_flags, signatures, offset);
+        let check = self.check_indirect_call_type_signature(table_index, ty_index, funcref_ptr);
+
+        let trap_code = match check {
+            // `funcref_ptr` is checked at runtime that its type matches,
+            // meaning that if code gets this far it's guaranteed to not be
+            // null. That means nothing in `unchecked_call` can fail.
+            CheckIndirectCallTypeSignature::Runtime => None,
+
+            // No type check was performed on `funcref_ptr` because it's
+            // statically known to have the right type. Note that whether or
+            // not the function is null is not necessarily tested so far since
+            // no type information was inspected.
+            //
+            // If the table may hold null functions, then further loads in
+            // `unchecked_call` may fail. If the table only holds non-null
+            // functions, though, then there's no possibility of a trap.
+            CheckIndirectCallTypeSignature::StaticMatch { may_be_null } => {
+                if may_be_null {
+                    Some(ir::TrapCode::IndirectCallToNull)
+                } else {
+                    None
+                }
+            }
 
-                // Load the callee ID.
+            // Code has already trapped, so return nothing indicating that this
+            // is now unreachable code.
+            CheckIndirectCallTypeSignature::StaticTrap => return Ok(None),
+        };
+        self.unchecked_call(sig_ref, funcref_ptr, trap_code, call_args)
+            .map(Some)
+    }
+
+    fn check_indirect_call_type_signature(
+        &mut self,
+        table_index: TableIndex,
+        ty_index: TypeIndex,
+        funcref_ptr: ir::Value,
+    ) -> CheckIndirectCallTypeSignature {
+        let pointer_type = self.env.pointer_type();
+        let table = &self.env.module.table_plans[table_index];
+        let sig_id_size = self.env.offsets.size_of_vmshared_type_index();
+        let sig_id_type = Type::int(u16::from(sig_id_size) * 8).unwrap();
+
+        // Generate a rustc compile error here if more styles are added in
+        // the future as the following code is tailored to just this style.
+        let TableStyle::CallerChecksSignature = table.style;
+
+        // Test if a type check is necessary for this table. If this table is a
+        // table of typed functions and that type matches `ty_index`, then
+        // there's no need to perform a typecheck.
+        match table.table.wasm_ty.heap_type {
+            // Functions do not have a statically known type in the table, a
+            // typecheck is required. Fall through to below to perform the
+            // actual typecheck.
+            WasmHeapType::Func => {}
+
+            // Functions that have a statically known type are either going to
+            // always succeed or always fail. Figure out by inspecting the types
+            // further.
+            WasmHeapType::Concrete(EngineOrModuleTypeIndex::Module(table_ty)) => {
+                // If `ty_index` matches `table_ty`, then this call is
+                // statically known to have the right type, so no checks are
+                // necessary.
+                let ModuleType::Function(specified_ty) = self.env.module.types[ty_index];
+                if specified_ty == table_ty {
+                    return CheckIndirectCallTypeSignature::StaticMatch {
+                        may_be_null: table.table.wasm_ty.nullable,
+                    };
+                }
+
+                // Otherwise if the types don't match then either (a) this is a
+                // null pointer or (b) it's a pointer with the wrong type.
+                // Figure out which and trap here.
                 //
-                // Note that the callee may be null in which case this load may
-                // trap. If so use the `IndirectCallToNull` trap code.
-                let mem_flags = ir::MemFlags::trusted().with_readonly();
-                let callee_sig_id = self.builder.ins().load(
-                    sig_id_type,
-                    mem_flags.with_trap_code(Some(ir::TrapCode::IndirectCallToNull)),
-                    funcref_ptr,
-                    i32::from(self.env.offsets.ptr.vm_func_ref_type_index()),
-                );
+                // If it's possible to have a null here then try to load the
+                // type information. If that fails due to the function being a
+                // null pointer, then this was a call to null. Otherwise if it
+                // succeeds then we know it won't match, so trap anyway.
+                if table.table.wasm_ty.nullable {
+                    let mem_flags = ir::MemFlags::trusted().with_readonly();
+                    self.builder.ins().load(
+                        sig_id_type,
+                        mem_flags.with_trap_code(Some(ir::TrapCode::IndirectCallToNull)),
+                        funcref_ptr,
+                        i32::from(self.env.offsets.ptr.vm_func_ref_type_index()),
+                    );
+                }
+                self.builder.ins().trap(ir::TrapCode::BadSignature);
+                return CheckIndirectCallTypeSignature::StaticTrap;
+            }
 
-                // Check that they match.
-                let cmp = self
-                    .builder
-                    .ins()
-                    .icmp(IntCC::Equal, callee_sig_id, caller_sig_id);
-                self.builder.ins().trapz(cmp, ir::TrapCode::BadSignature);
+            // Tables of `nofunc` can only be inhabited by null, so go ahead and
+            // trap with that.
+            WasmHeapType::NoFunc => {
+                assert!(table.table.wasm_ty.nullable);
+                self.builder.ins().trap(ir::TrapCode::IndirectCallToNull);
+                return CheckIndirectCallTypeSignature::StaticTrap;
+            }
+
+            // Engine-indexed types don't show up until runtime and it's a wasm
+            // validation error to perform a call through an `externref` table,
+            // so these cases are dynamically not reachable.
+            WasmHeapType::Concrete(EngineOrModuleTypeIndex::Engine(_)) | WasmHeapType::Extern => {
+                unreachable!()
             }
         }
 
-        // Note that `funcref_ptr` at this point is guaranteed to not be null as
-        // we've loaded its type which would have otherwise faulted. That means
-        // that further loads no longer need trap metadata, so use `None` here
-        // for the next trap code.
-        self.unchecked_call(sig_ref, funcref_ptr, None, call_args)
+        let vmctx = self.env.vmctx(self.builder.func);
+        let base = self.builder.ins().global_value(pointer_type, vmctx);
+
+        // Load the caller ID. This requires loading the `*mut VMFuncRef` base
+        // pointer from `VMContext` and then loading, based on `SignatureIndex`,
+        // the corresponding entry.
+        let mem_flags = ir::MemFlags::trusted().with_readonly();
+        let signatures = self.builder.ins().load(
+            pointer_type,
+            mem_flags,
+            base,
+            i32::try_from(self.env.offsets.vmctx_type_ids_array()).unwrap(),
+        );
+        let sig_index = self.env.module.types[ty_index].unwrap_function();
+        let offset =
+            i32::try_from(sig_index.as_u32().checked_mul(sig_id_type.bytes()).unwrap()).unwrap();
+        let caller_sig_id = self
+            .builder
+            .ins()
+            .load(sig_id_type, mem_flags, signatures, offset);
+
+        // Load the callee ID.
+        //
+        // Note that the callee may be null in which case this load may
+        // trap. If so use the `IndirectCallToNull` trap code.
+        let mem_flags = ir::MemFlags::trusted().with_readonly();
+        let callee_sig_id = self.builder.ins().load(
+            sig_id_type,
+            mem_flags.with_trap_code(Some(ir::TrapCode::IndirectCallToNull)),
+            funcref_ptr,
+            i32::from(self.env.offsets.ptr.vm_func_ref_type_index()),
+        );
+
+        // Check that they match.
+        let cmp = self
+            .builder
+            .ins()
+            .icmp(IntCC::Equal, callee_sig_id, caller_sig_id);
+        self.builder.ins().trapz(cmp, ir::TrapCode::BadSignature);
+        CheckIndirectCallTypeSignature::Runtime
     }
 
     /// Call a typed function reference.
@@ -2234,7 +2337,7 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
         sig_ref: ir::SigRef,
         callee: ir::Value,
         call_args: &[ir::Value],
-    ) -> WasmResult<ir::Inst> {
+    ) -> WasmResult<Option<ir::Inst>> {
         Call::new(builder, self).indirect_call(table_index, ty_index, sig_ref, callee, call_args)
     }
 
diff --git a/crates/environ/src/trap_encoding.rs b/crates/environ/src/trap_encoding.rs
index 0202631d9..d14382a8b 100644
--- a/crates/environ/src/trap_encoding.rs
+++ b/crates/environ/src/trap_encoding.rs
@@ -98,6 +98,42 @@ pub enum Trap {
     // if adding a variant here be sure to update the `check!` macro below
 }
 
+impl Trap {
+    /// Converts a byte back into a `Trap` if its in-bounds
+    pub fn from_u8(byte: u8) -> Option<Trap> {
+        // FIXME: this could use some sort of derive-like thing to avoid having to
+        // deduplicate the names here.
+        //
+        // This simply converts from the a `u8`, to the `Trap` enum.
+        macro_rules! check {
+            ($($name:ident)*) => ($(if byte == Trap::$name as u8 {
+                return Some(Trap::$name);
+            })*);
+        }
+
+        check! {
+            StackOverflow
+            MemoryOutOfBounds
+            HeapMisaligned
+            TableOutOfBounds
+            IndirectCallToNull
+            BadSignature
+            IntegerOverflow
+            IntegerDivisionByZero
+            BadConversionToInteger
+            UnreachableCodeReached
+            Interrupt
+            AlwaysTrapAdapter
+            OutOfFuel
+            AtomicWaitNonSharedMemory
+            NullReference
+            CannotEnterComponent
+        }
+
+        None
+    }
+}
+
 impl fmt::Display for Trap {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         use Trap::*;
@@ -206,40 +242,9 @@ pub fn lookup_trap_code(section: &[u8], offset: usize) -> Option<Trap> {
         .binary_search_by_key(&offset, |val| val.get(LittleEndian))
         .ok()?;
     debug_assert!(index < traps.len());
-    let trap = *traps.get(index)?;
+    let byte = *traps.get(index)?;
 
-    // FIXME: this could use some sort of derive-like thing to avoid having to
-    // deduplicate the names here.
-    //
-    // This simply converts from the `trap`, a `u8`, to the `Trap` enum.
-    macro_rules! check {
-        ($($name:ident)*) => ($(if trap == Trap::$name as u8 {
-            return Some(Trap::$name);
-        })*);
-    }
-
-    check! {
-        StackOverflow
-        MemoryOutOfBounds
-        HeapMisaligned
-        TableOutOfBounds
-        IndirectCallToNull
-        BadSignature
-        IntegerOverflow
-        IntegerDivisionByZero
-        BadConversionToInteger
-        UnreachableCodeReached
-        Interrupt
-        AlwaysTrapAdapter
-        OutOfFuel
-        AtomicWaitNonSharedMemory
-        NullReference
-        CannotEnterComponent
-    }
-
-    if cfg!(debug_assertions) {
-        panic!("missing mapping for {}", trap);
-    } else {
-        None
-    }
+    let trap = Trap::from_u8(byte);
+    debug_assert!(trap.is_some(), "missing mapping for {}", byte);
+    trap
 }
diff --git a/crates/runtime/src/sys/custom/traphandlers.rs b/crates/runtime/src/sys/custom/traphandlers.rs
index 7c2508c50..65ea6afe5 100644
--- a/crates/runtime/src/sys/custom/traphandlers.rs
+++ b/crates/runtime/src/sys/custom/traphandlers.rs
@@ -1,4 +1,4 @@
-use crate::traphandlers::tls;
+use crate::traphandlers::{tls, TrapTest};
 use crate::VMContext;
 use std::mem;
 
@@ -38,12 +38,16 @@ extern "C" fn handle_trap(ip: usize, fp: usize, has_faulting_addr: bool, faultin
             None
         };
         let ip = ip as *const u8;
-        let jmp_buf = info.take_jmp_buf_if_trap(ip, |_handler| {
+        let test = info.test_if_trap(ip, |_handler| {
             panic!("custom signal handlers are not supported on this platform");
         });
-        if !jmp_buf.is_null() {
-            info.set_jit_trap(ip, fp, faulting_addr);
-            unsafe { wasmtime_longjmp(jmp_buf) }
+        match test {
+            TrapTest::NotWasm => {}
+            TrapTest::HandledByEmbedder => unreachable!(),
+            TrapTest::Trap { jmp_buf, trap } => {
+                info.set_jit_trap(ip, fp, faulting_addr, trap);
+                unsafe { wasmtime_longjmp(jmp_buf) }
+            }
         }
     })
 }
diff --git a/crates/runtime/src/sys/unix/machports.rs b/crates/runtime/src/sys/unix/machports.rs
index 3b6f8dfa5..83a4c5037 100644
--- a/crates/runtime/src/sys/unix/machports.rs
+++ b/crates/runtime/src/sys/unix/machports.rs
@@ -49,6 +49,7 @@ use mach2::traps::*;
 use std::mem;
 use std::ptr::addr_of_mut;
 use std::thread;
+use wasmtime_environ::Trap;
 
 /// Process-global port that we use to route thread-level exceptions to.
 static mut WASMTIME_PORT: mach_port_name_t = MACH_PORT_NULL;
@@ -195,10 +196,10 @@ unsafe fn handle_exception(request: &mut ExceptionRequest) -> bool {
     // the second `code` field. If we're ever interested in it the first code
     // field has a `kern_return_t` describing the kind of failure (e.g. SIGSEGV
     // vs SIGBUS), but we're not interested in that right now.
-    let (fault1, fault2) = if request.body.exception as u32 == EXC_BAD_ACCESS {
-        (1, request.body.code[1] as usize)
+    let faulting_addr = if request.body.exception as u32 == EXC_BAD_ACCESS {
+        Some(request.body.code[1] as usize)
     } else {
-        (0, 0)
+        None
     };
 
     // Depending on the current architecture various bits and pieces of this
@@ -232,7 +233,7 @@ unsafe fn handle_exception(request: &mut ExceptionRequest) -> bool {
                 state.__rbp as usize,
             );
 
-            let resume = |state: &mut ThreadState, pc: usize, fp: usize, fault1: usize, fault2: usize| {
+            let resume = |state: &mut ThreadState, pc: usize, fp: usize, fault1: usize, fault2: usize, trap: Trap| {
                 // The x86_64 ABI requires a 16-byte stack alignment for
                 // functions, so typically we'll be 16-byte aligned. In this
                 // case we simulate a `call` instruction by decrementing the
@@ -261,6 +262,7 @@ unsafe fn handle_exception(request: &mut ExceptionRequest) -> bool {
                 state.__rsi = fp as u64;
                 state.__rdx = fault1 as u64;
                 state.__rcx = fault2 as u64;
+                state.__r8 = trap as u64;
             };
             let mut thread_state = ThreadState::new();
         } else if #[cfg(target_arch = "aarch64")] {
@@ -273,7 +275,7 @@ unsafe fn handle_exception(request: &mut ExceptionRequest) -> bool {
                 state.__fp as usize,
             );
 
-            let resume = |state: &mut ThreadState, pc: usize, fp: usize, fault1: usize, fault2: usize| {
+            let resume = |state: &mut ThreadState, pc: usize, fp: usize, fault1: usize, fault2: usize, trap: Trap| {
                 // Clobber LR with the faulting PC, so unwinding resumes at the
                 // faulting instruction. The previous value of LR has been saved
                 // by the callee (in Cranelift generated code), so no need to
@@ -286,6 +288,7 @@ unsafe fn handle_exception(request: &mut ExceptionRequest) -> bool {
                 state.__x[1] = fp as u64;
                 state.__x[2] = fault1 as u64;
                 state.__x[3] = fault2 as u64;
+                state.__x[4] = trap as u64;
                 state.__pc = unwind as u64;
             };
             let mut thread_state = mem::zeroed::<ThreadState>();
@@ -322,15 +325,20 @@ unsafe fn handle_exception(request: &mut ExceptionRequest) -> bool {
     // pointer value and if `MAP` changes happen after we read our entry that's
     // ok since they won't invalidate our entry.
     let (pc, fp) = get_pc_and_fp(&thread_state);
-    if !crate::traphandlers::IS_WASM_PC(pc as usize) {
-        return false;
-    }
+    let trap = match crate::traphandlers::GET_WASM_TRAP(pc as usize) {
+        Some(trap) => trap,
+        None => return false,
+    };
 
     // We have determined that this is a wasm trap and we need to actually
     // force the thread itself to trap. The thread's register state is
     // configured to resume in the `unwind` function below, we update the
     // thread's register state, and then we're off to the races.
-    resume(&mut thread_state, pc as usize, fp, fault1, fault2);
+    let (fault1, fault2) = match faulting_addr {
+        None => (0, 0),
+        Some(addr) => (1, addr),
+    };
+    resume(&mut thread_state, pc as usize, fp, fault1, fault2, trap);
     let kret = thread_set_state(
         origin_thread,
         thread_state_flavor,
@@ -350,17 +358,18 @@ unsafe fn handle_exception(request: &mut ExceptionRequest) -> bool {
 unsafe extern "C" fn unwind(
     wasm_pc: *const u8,
     wasm_fp: usize,
-    has_faulting_addr: usize,
-    faulting_addr: usize,
+    fault1: usize,
+    fault2: usize,
+    trap: u8,
 ) -> ! {
     let jmp_buf = tls::with(|state| {
         let state = state.unwrap();
-        let faulting_addr = if has_faulting_addr != 0 {
-            Some(faulting_addr)
-        } else {
-            None
+        let faulting_addr = match fault1 {
+            0 => None,
+            _ => Some(fault2),
         };
-        state.set_jit_trap(wasm_pc, wasm_fp, faulting_addr);
+        let trap = Trap::from_u8(trap).unwrap();
+        state.set_jit_trap(wasm_pc, wasm_fp, faulting_addr, trap);
         state.take_jmp_buf()
     });
     debug_assert!(!jmp_buf.is_null());
diff --git a/crates/runtime/src/sys/unix/signals.rs b/crates/runtime/src/sys/unix/signals.rs
index df6f25a40..45961a0ea 100644
--- a/crates/runtime/src/sys/unix/signals.rs
+++ b/crates/runtime/src/sys/unix/signals.rs
@@ -1,6 +1,6 @@
 //! Trap handling on Unix based on POSIX signals.
 
-use crate::traphandlers::tls;
+use crate::traphandlers::{tls, TrapTest};
 use crate::VMContext;
 use std::cell::RefCell;
 use std::io;
@@ -111,23 +111,22 @@ unsafe extern "C" fn trap_handler(
         // handling, and reset our trap handling flag. Then we figure
         // out what to do based on the result of the trap handling.
         let (pc, fp) = get_pc_and_fp(context, signum);
-        let jmp_buf = info.take_jmp_buf_if_trap(pc, |handler| handler(signum, siginfo, context));
+        let test = info.test_if_trap(pc, |handler| handler(signum, siginfo, context));
 
         // Figure out what to do based on the result of this handling of
         // the trap. Note that our sentinel value of 1 means that the
         // exception was handled by a custom exception handler, so we
         // keep executing.
-        if jmp_buf.is_null() {
-            return false;
-        }
-        if jmp_buf as usize == 1 {
-            return true;
-        }
+        let (jmp_buf, trap) = match test {
+            TrapTest::NotWasm => return false,
+            TrapTest::HandledByEmbedder => return true,
+            TrapTest::Trap { jmp_buf, trap } => (jmp_buf, trap),
+        };
         let faulting_addr = match signum {
             libc::SIGSEGV | libc::SIGBUS => Some((*siginfo).si_addr() as usize),
             _ => None,
         };
-        info.set_jit_trap(pc, fp, faulting_addr);
+        info.set_jit_trap(pc, fp, faulting_addr, trap);
         // On macOS this is a bit special, unfortunately. If we were to
         // `siglongjmp` out of the signal handler that notably does
         // *not* reset the sigaltstack state of our signal handler. This
diff --git a/crates/runtime/src/sys/windows/traphandlers.rs b/crates/runtime/src/sys/windows/traphandlers.rs
index f383477ff..6563d3986 100644
--- a/crates/runtime/src/sys/windows/traphandlers.rs
+++ b/crates/runtime/src/sys/windows/traphandlers.rs
@@ -1,4 +1,4 @@
-use crate::traphandlers::tls;
+use crate::traphandlers::{tls, TrapTest};
 use crate::VMContext;
 use std::io;
 use windows_sys::Win32::Foundation::*;
@@ -88,14 +88,13 @@ unsafe extern "system" fn exception_handler(exception_info: *mut EXCEPTION_POINT
         } else {
             None
         };
-        let jmp_buf = info.take_jmp_buf_if_trap(ip, |handler| handler(exception_info));
-        if jmp_buf.is_null() {
-            ExceptionContinueSearch
-        } else if jmp_buf as usize == 1 {
-            ExceptionContinueExecution
-        } else {
-            info.set_jit_trap(ip, fp, faulting_addr);
-            wasmtime_longjmp(jmp_buf)
+        match info.test_if_trap(ip, |handler| handler(exception_info)) {
+            TrapTest::NotWasm => ExceptionContinueSearch,
+            TrapTest::HandledByEmbedder => ExceptionContinueExecution,
+            TrapTest::Trap { jmp_buf, trap } => {
+                info.set_jit_trap(ip, fp, faulting_addr, trap);
+                wasmtime_longjmp(jmp_buf)
+            }
         }
     })
 }
diff --git a/crates/runtime/src/traphandlers.rs b/crates/runtime/src/traphandlers.rs
index 88cbf5609..06855d307 100644
--- a/crates/runtime/src/traphandlers.rs
+++ b/crates/runtime/src/traphandlers.rs
@@ -24,7 +24,7 @@ pub use traphandlers::SignalHandler;
 ///
 /// This is initialized during `init_traps` below. The definition lives within
 /// `wasmtime` currently.
-pub(crate) static mut IS_WASM_PC: fn(usize) -> bool = |_| false;
+pub(crate) static mut GET_WASM_TRAP: fn(usize) -> Option<wasmtime_environ::Trap> = |_| None;
 
 /// This function is required to be called before any WebAssembly is entered.
 /// This will configure global state such as signal handlers to prepare the
@@ -39,11 +39,14 @@ pub(crate) static mut IS_WASM_PC: fn(usize) -> bool = |_| false;
 /// program counter is the pc of an actual wasm trap or not. This is then used
 /// to disambiguate faults that happen due to wasm and faults that happen due to
 /// bugs in Rust or elsewhere.
-pub fn init_traps(is_wasm_pc: fn(usize) -> bool, macos_use_mach_ports: bool) {
+pub fn init_traps(
+    get_wasm_trap: fn(usize) -> Option<wasmtime_environ::Trap>,
+    macos_use_mach_ports: bool,
+) {
     static INIT: Once = Once::new();
 
     INIT.call_once(|| unsafe {
-        IS_WASM_PC = is_wasm_pc;
+        GET_WASM_TRAP = get_wasm_trap;
         traphandlers::platform_init(macos_use_mach_ports);
     });
 
@@ -157,6 +160,9 @@ pub enum TrapReason {
         /// fault-based traps which are one of the main ways, but not the only
         /// way, to run wasm.
         faulting_addr: Option<usize>,
+
+        /// The trap code associated with this trap.
+        trap: wasmtime_environ::Trap,
     },
 
     /// A trap raised from a wasm libcall
@@ -198,6 +204,22 @@ impl From<wasmtime_environ::Trap> for TrapReason {
     }
 }
 
+/// Return value from `test_if_trap`.
+pub(crate) enum TrapTest {
+    /// Not a wasm trap, need to delegate to whatever process handler is next.
+    NotWasm,
+    /// This trap was handled by the embedder via custom embedding APIs.
+    HandledByEmbedder,
+    /// This is a wasm trap, it needs to be handled.
+    #[cfg_attr(miri, allow(dead_code))]
+    Trap {
+        /// How to longjmp back to the original wasm frame.
+        jmp_buf: *const u8,
+        /// The trap code of this trap.
+        trap: wasmtime_environ::Trap,
+    },
+}
+
 /// Catches any wasm traps that happen within the execution of `closure`,
 /// returning them as a `Result`.
 ///
@@ -406,14 +428,14 @@ impl CallThreadState {
     /// * a different pointer - a jmp_buf buffer to longjmp to, meaning that
     ///   the wasm trap was succesfully handled.
     #[cfg_attr(miri, allow(dead_code))] // miri doesn't handle traps yet
-    pub(crate) fn take_jmp_buf_if_trap(
+    pub(crate) fn test_if_trap(
         &self,
         pc: *const u8,
         call_handler: impl Fn(&SignalHandler) -> bool,
-    ) -> *const u8 {
+    ) -> TrapTest {
         // If we haven't even started to handle traps yet, bail out.
         if self.jmp_buf.get().is_null() {
-            return ptr::null();
+            return TrapTest::NotWasm;
         }
 
         // First up see if any instance registered has a custom trap handler,
@@ -421,18 +443,22 @@ impl CallThreadState {
         // return that the trap was handled.
         if let Some(handler) = self.signal_handler {
             if unsafe { call_handler(&*handler) } {
-                return 1 as *const _;
+                return TrapTest::HandledByEmbedder;
             }
         }
 
         // If this fault wasn't in wasm code, then it's not our problem
-        if unsafe { !IS_WASM_PC(pc as usize) } {
-            return ptr::null();
-        }
+        let trap = match unsafe { GET_WASM_TRAP(pc as usize) } {
+            Some(trap) => trap,
+            None => return TrapTest::NotWasm,
+        };
 
         // If all that passed then this is indeed a wasm trap, so return the
         // `jmp_buf` passed to `wasmtime_longjmp` to resume.
-        self.take_jmp_buf()
+        TrapTest::Trap {
+            jmp_buf: self.take_jmp_buf(),
+            trap,
+        }
     }
 
     pub(crate) fn take_jmp_buf(&self) -> *const u8 {
@@ -440,7 +466,13 @@ impl CallThreadState {
     }
 
     #[cfg_attr(miri, allow(dead_code))] // miri doesn't handle traps yet
-    pub(crate) fn set_jit_trap(&self, pc: *const u8, fp: usize, faulting_addr: Option<usize>) {
+    pub(crate) fn set_jit_trap(
+        &self,
+        pc: *const u8,
+        fp: usize,
+        faulting_addr: Option<usize>,
+        trap: wasmtime_environ::Trap,
+    ) {
         let backtrace = self.capture_backtrace(self.limits, Some((pc as usize, fp)));
         let coredump = self.capture_coredump(self.limits, Some((pc as usize, fp)));
         unsafe {
@@ -448,6 +480,7 @@ impl CallThreadState {
                 UnwindReason::Trap(TrapReason::Jit {
                     pc: pc as usize,
                     faulting_addr,
+                    trap,
                 }),
                 backtrace,
                 coredump,
diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs
index b6ec980aa..aa69554a8 100644
--- a/crates/wasmtime/src/engine.rs
+++ b/crates/wasmtime/src/engine.rs
@@ -86,10 +86,7 @@ impl Engine {
             // Ensure that wasmtime_runtime's signal handlers are configured. This
             // is the per-program initialization required for handling traps, such
             // as configuring signals, vectored exception handlers, etc.
-            wasmtime_runtime::init_traps(
-                crate::module::is_wasm_trap_pc,
-                config.macos_use_mach_ports,
-            );
+            wasmtime_runtime::init_traps(crate::module::get_wasm_trap, config.macos_use_mach_ports);
             #[cfg(feature = "debug-builtins")]
             wasmtime_runtime::debug_builtins::ensure_exported();
         }
diff --git a/crates/wasmtime/src/runtime/module.rs b/crates/wasmtime/src/runtime/module.rs
index ca59e7070..3cc8aae5b 100644
--- a/crates/wasmtime/src/runtime/module.rs
+++ b/crates/wasmtime/src/runtime/module.rs
@@ -28,7 +28,7 @@ use wasmtime_runtime::{
 mod registry;
 
 pub use registry::{
-    is_wasm_trap_pc, register_code, unregister_code, ModuleRegistry, RegisteredModuleId,
+    get_wasm_trap, register_code, unregister_code, ModuleRegistry, RegisteredModuleId,
 };
 
 /// A compiled WebAssembly module, ready to be instantiated.
diff --git a/crates/wasmtime/src/runtime/module/registry.rs b/crates/wasmtime/src/runtime/module/registry.rs
index 9c8b50528..2c8e7081b 100644
--- a/crates/wasmtime/src/runtime/module/registry.rs
+++ b/crates/wasmtime/src/runtime/module/registry.rs
@@ -34,9 +34,9 @@ pub struct ModuleRegistry {
 }
 
 struct LoadedCode {
-    /// Representation of loaded code which could be either a component or a
-    /// module.
-    code: Arc<CodeObject>,
+    /// Kept alive here in the store to have a strong reference to keep the
+    /// relevant code mapped while the store is alive.
+    _code: Arc<CodeObject>,
 
     /// Modules found within `self.code`, keyed by start address here of the
     /// address of the first function in the module.
@@ -153,7 +153,7 @@ impl ModuleRegistry {
         }
 
         let mut item = LoadedCode {
-            code: code.clone(),
+            _code: code.clone(),
             modules: Default::default(),
         };
         if let Some(module) = module {
@@ -164,12 +164,6 @@ impl ModuleRegistry {
         id
     }
 
-    /// Fetches trap information about a program counter in a backtrace.
-    pub fn lookup_trap_code(&self, pc: usize) -> Option<Trap> {
-        let (code, offset) = self.code(pc)?;
-        wasmtime_environ::lookup_trap_code(code.code.code_memory().trap_data(), offset)
-    }
-
     /// Fetches frame information about a program counter in a backtrace.
     ///
     /// Returns an object if this `pc` is known to some previously registered
@@ -260,21 +254,21 @@ type GlobalRegistry = BTreeMap<usize, (usize, Arc<CodeMemory>)>;
 
 /// Returns whether the `pc`, according to globally registered information,
 /// is a wasm trap or not.
-pub fn is_wasm_trap_pc(pc: usize) -> bool {
+pub fn get_wasm_trap(pc: usize) -> Option<Trap> {
     let (code, text_offset) = {
         let all_modules = GLOBAL_CODE.read().unwrap();
 
         let (end, (start, module)) = match all_modules.range(pc..).next() {
             Some(info) => info,
-            None => return false,
+            None => return None,
         };
         if pc < *start || *end < pc {
-            return false;
+            return None;
         }
         (module.clone(), pc - *start)
     };
 
-    wasmtime_environ::lookup_trap_code(code.trap_data(), text_offset).is_some()
+    wasmtime_environ::lookup_trap_code(code.trap_data(), text_offset)
 }
 
 /// Registers a new region of code.
diff --git a/crates/wasmtime/src/runtime/trap.rs b/crates/wasmtime/src/runtime/trap.rs
index ba4bcaeff..8d5b52188 100644
--- a/crates/wasmtime/src/runtime/trap.rs
+++ b/crates/wasmtime/src/runtime/trap.rs
@@ -109,12 +109,12 @@ pub(crate) fn from_runtime_box(
             );
             (error, None)
         }
-        wasmtime_runtime::TrapReason::Jit { pc, faulting_addr } => {
-            let code = store
-                .modules()
-                .lookup_trap_code(pc)
-                .unwrap_or(Trap::StackOverflow);
-            let mut err: Error = code.into();
+        wasmtime_runtime::TrapReason::Jit {
+            pc,
+            faulting_addr,
+            trap,
+        } => {
+            let mut err: Error = trap.into();
 
             // If a fault address was present, for example with segfaults,
             // then simultaneously assert that it's within a known linear memory
diff --git a/src/commands/run.rs b/src/commands/run.rs
index 10c97649a..1413874ee 100644
--- a/src/commands/run.rs
+++ b/src/commands/run.rs
@@ -607,7 +607,22 @@ impl RunCommand {
         store: &mut Store<Host>,
         module: &RunTarget,
     ) -> Result<()> {
-        if self.run.common.wasi.common != Some(false) {
+        let mut cli = self.run.common.wasi.cli;
+
+        // Accept -Scommon as a deprecated alias for -Scli.
+        if let Some(common) = self.run.common.wasi.common {
+            if cli.is_some() {
+                bail!(
+                    "The -Scommon option should not be use with -Scli as it is a deprecated alias"
+                );
+            } else {
+                // In the future, we may add a warning here to tell users to use
+                // `-S cli` instead of `-S common`.
+                cli = Some(common);
+            }
+        }
+
+        if cli != Some(false) {
             match linker {
                 CliLinker::Core(linker) => {
                     match (self.run.common.wasi.preview2, self.run.common.wasi.threads) {
diff --git a/src/commands/serve.rs b/src/commands/serve.rs
index e59f91839..c35182b59 100644
--- a/src/commands/serve.rs
+++ b/src/commands/serve.rs
@@ -191,16 +191,30 @@ impl ServeCommand {
     }
 
     fn add_to_linker(&self, linker: &mut Linker<Host>) -> Result<()> {
-        // Repurpose the `-Scommon` flag of `wasmtime run` for `wasmtime serve`
+        let mut cli = self.run.common.wasi.cli;
+
+        // Accept -Scommon as a deprecated alias for -Scli.
+        if let Some(common) = self.run.common.wasi.common {
+            if cli.is_some() {
+                bail!(
+                    "The -Scommon option should not be use with -Scli as it is a deprecated alias"
+                );
+            } else {
+                // In the future, we may add a warning here to tell users to use
+                // `-S cli` instead of `-S common`.
+                cli = Some(common);
+            }
+        }
+
+        // Repurpose the `-Scli` flag of `wasmtime run` for `wasmtime serve`
         // to serve as a signal to enable all WASI interfaces instead of just
-        // those in the `proxy` world. If `-Scommon` is present then add all
-        // `command` APIs which includes all "common" or base WASI APIs and then
-        // additionally add in the required HTTP APIs.
+        // those in the `proxy` world. If `-Scli` is present then add all
+        // `command` APIs and then additionally add in the required HTTP APIs.
         //
-        // If `-Scommon` isn't passed then use the `proxy::add_to_linker`
+        // If `-Scli` isn't passed then use the `proxy::add_to_linker`
         // bindings which adds just those interfaces that the proxy interface
         // uses.
-        if self.run.common.wasi.common == Some(true) {
+        if cli == Some(true) {
             wasmtime_wasi::command::add_to_linker(linker)?;
             wasmtime_wasi_http::proxy::add_only_http_to_linker(linker)?;
         } else {
diff --git a/src/old_cli.rs b/src/old_cli.rs
index f6bbe5ff4..c863fc1fd 100644
--- a/src/old_cli.rs
+++ b/src/old_cli.rs
@@ -981,7 +981,7 @@ impl CommonOptions {
             ret.wasi.http = wasi_http;
             ret.wasi.nn = wasi_nn;
             ret.wasi.threads = wasi_threads;
-            ret.wasi.common = wasi_common;
+            ret.wasi.cli = wasi_common;
         }
         ret
     }


Test patch:
diff --git a/cranelift/filetests/filetests/isa/x64/insertlane-avx.clif b/cranelift/filetests/filetests/isa/x64/insertlane-avx.clif
index 7b1368a9b..5f137321d 100644
--- a/cranelift/filetests/filetests/isa/x64/insertlane-avx.clif
+++ b/cranelift/filetests/filetests/isa/x64/insertlane-avx.clif
@@ -62,8 +62,8 @@ block0(v0: f64x2, v1: i64):
 ;   pushq   %rbp
 ;   movq    %rsp, %rbp
 ; block0:
-;   vmovsd  0(%rdi), %xmm3
-;   vmovsd  %xmm0, %xmm3, %xmm0
+;   vmovsd  0(%rdi), %xmm4
+;   vmovsd  %xmm0, %xmm4, %xmm0
 ;   movq    %rbp, %rsp
 ;   popq    %rbp
 ;   ret
@@ -73,8 +73,8 @@ block0(v0: f64x2, v1: i64):
 ;   pushq %rbp
 ;   movq %rsp, %rbp
 ; block1: ; offset 0x4
-;   vmovsd (%rdi), %xmm3 ; trap: heap_oob
-;   vmovsd %xmm3, %xmm0, %xmm0
+;   vmovsd (%rdi), %xmm4 ; trap: heap_oob
+;   vmovsd %xmm4, %xmm0, %xmm0
 ;   movq %rbp, %rsp
 ;   popq %rbp
 ;   retq
@@ -90,8 +90,7 @@ block0(v0: i8x16, v1: i64):
 ;   pushq   %rbp
 ;   movq    %rsp, %rbp
 ; block0:
-;   movzbq  0(%rdi), %rdx
-;   vpinsrb $1, %xmm0, %rdx, %xmm0
+;   vpinsrb $1, %xmm0, 0(%rdi), %xmm0
 ;   movq    %rbp, %rsp
 ;   popq    %rbp
 ;   ret
@@ -101,8 +100,7 @@ block0(v0: i8x16, v1: i64):
 ;   pushq %rbp
 ;   movq %rsp, %rbp
 ; block1: ; offset 0x4
-;   movzbq (%rdi), %rdx ; trap: heap_oob
-;   vpinsrb $1, %edx, %xmm0, %xmm0
+;   vpinsrb $1, (%rdi), %xmm0, %xmm0 ; trap: heap_oob
 ;   movq %rbp, %rsp
 ;   popq %rbp
 ;   retq
@@ -118,8 +116,7 @@ block0(v0: i16x8, v1: i64):
 ;   pushq   %rbp
 ;   movq    %rsp, %rbp
 ; block0:
-;   movzwq  0(%rdi), %rdx
-;   vpinsrw $1, %xmm0, %rdx, %xmm0
+;   vpinsrw $1, %xmm0, 0(%rdi), %xmm0
 ;   movq    %rbp, %rsp
 ;   popq    %rbp
 ;   ret
@@ -129,8 +126,7 @@ block0(v0: i16x8, v1: i64):
 ;   pushq %rbp
 ;   movq %rsp, %rbp
 ; block1: ; offset 0x4
-;   movzwq (%rdi), %rdx ; trap: heap_oob
-;   vpinsrw $1, %edx, %xmm0, %xmm0
+;   vpinsrw $1, (%rdi), %xmm0, %xmm0 ; trap: heap_oob
 ;   movq %rbp, %rsp
 ;   popq %rbp
 ;   retq
diff --git a/cranelift/filetests/filetests/isa/x64/insertlane.clif b/cranelift/filetests/filetests/isa/x64/insertlane.clif
index d15afb787..d27740f58 100644
--- a/cranelift/filetests/filetests/isa/x64/insertlane.clif
+++ b/cranelift/filetests/filetests/isa/x64/insertlane.clif
@@ -62,8 +62,8 @@ block0(v0: f64x2, v1: i64):
 ;   pushq   %rbp
 ;   movq    %rsp, %rbp
 ; block0:
-;   movsd   0(%rdi), %xmm3
-;   movsd   %xmm0, %xmm3, %xmm0
+;   movsd   0(%rdi), %xmm4
+;   movsd   %xmm0, %xmm4, %xmm0
 ;   movq    %rbp, %rsp
 ;   popq    %rbp
 ;   ret
@@ -73,8 +73,8 @@ block0(v0: f64x2, v1: i64):
 ;   pushq %rbp
 ;   movq %rsp, %rbp
 ; block1: ; offset 0x4
-;   movsd (%rdi), %xmm3 ; trap: heap_oob
-;   movsd %xmm3, %xmm0
+;   movsd (%rdi), %xmm4 ; trap: heap_oob
+;   movsd %xmm4, %xmm0
 ;   movq %rbp, %rsp
 ;   popq %rbp
 ;   retq
@@ -90,8 +90,7 @@ block0(v0: i8x16, v1: i64):
 ;   pushq   %rbp
 ;   movq    %rsp, %rbp
 ; block0:
-;   movzbq  0(%rdi), %rdx
-;   pinsrb  $1, %xmm0, %rdx, %xmm0
+;   pinsrb  $1, %xmm0, 0(%rdi), %xmm0
 ;   movq    %rbp, %rsp
 ;   popq    %rbp
 ;   ret
@@ -101,8 +100,7 @@ block0(v0: i8x16, v1: i64):
 ;   pushq %rbp
 ;   movq %rsp, %rbp
 ; block1: ; offset 0x4
-;   movzbq (%rdi), %rdx ; trap: heap_oob
-;   pinsrb $1, %edx, %xmm0
+;   pinsrb $1, (%rdi), %xmm0 ; trap: heap_oob
 ;   movq %rbp, %rsp
 ;   popq %rbp
 ;   retq
@@ -118,8 +116,7 @@ block0(v0: i16x8, v1: i64):
 ;   pushq   %rbp
 ;   movq    %rsp, %rbp
 ; block0:
-;   movzwq  0(%rdi), %rdx
-;   pinsrw  $1, %xmm0, %rdx, %xmm0
+;   pinsrw  $1, %xmm0, 0(%rdi), %xmm0
 ;   movq    %rbp, %rsp
 ;   popq    %rbp
 ;   ret
@@ -129,8 +126,7 @@ block0(v0: i16x8, v1: i64):
 ;   pushq %rbp
 ;   movq %rsp, %rbp
 ; block1: ; offset 0x4
-;   movzwq (%rdi), %rdx ; trap: heap_oob
-;   pinsrw $1, %edx, %xmm0
+;   pinsrw $1, (%rdi), %xmm0 ; trap: heap_oob
 ;   movq %rbp, %rsp
 ;   popq %rbp
 ;   retq
diff --git a/cranelift/filetests/filetests/isa/x64/simd-lane-access-compile.clif b/cranelift/filetests/filetests/isa/x64/simd-lane-access-compile.clif
index 3331c7765..5a84fa914 100644
--- a/cranelift/filetests/filetests/isa/x64/simd-lane-access-compile.clif
+++ b/cranelift/filetests/filetests/isa/x64/simd-lane-access-compile.clif
@@ -340,3 +340,135 @@ block0(v0: f32):
 ;   popq %rbp
 ;   retq
 
+
+
+function %load32_lane_coalesced(i64, i32x4) -> i32x4 {
+block0(v0: i64, v1: i32x4):
+    v2 = load.i32 v0
+    v3 = insertlane.i32x4 v1, v2, 3
+    return v3
+}
+
+; VCode:
+;   pushq   %rbp
+;   movq    %rsp, %rbp
+; block0:
+;   pinsrd  $3, %xmm0, 0(%rdi), %xmm0
+;   movq    %rbp, %rsp
+;   popq    %rbp
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   pushq %rbp
+;   movq %rsp, %rbp
+; block1: ; offset 0x4
+;   pinsrd $3, (%rdi), %xmm0 ; trap: heap_oob
+;   movq %rbp, %rsp
+;   popq %rbp
+;   retq
+
+function %load16_lane_coalesced(i64, i16x8) -> i16x8 {
+block0(v0: i64, v1: i16x8):
+    v2 = load.i16 v0
+    v3 = insertlane.i16x8 v1, v2, 3
+    return v3
+}
+
+; VCode:
+;   pushq   %rbp
+;   movq    %rsp, %rbp
+; block0:
+;   pinsrw  $3, %xmm0, 0(%rdi), %xmm0
+;   movq    %rbp, %rsp
+;   popq    %rbp
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   pushq %rbp
+;   movq %rsp, %rbp
+; block1: ; offset 0x4
+;   pinsrw $3, (%rdi), %xmm0 ; trap: heap_oob
+;   movq %rbp, %rsp
+;   popq %rbp
+;   retq
+
+function %load8_lane_coalesced(i64, i8x16) -> i8x16 {
+block0(v0: i64, v1: i8x16):
+    v2 = load.i8 v0
+    v3 = insertlane.i8x16 v1, v2, 3
+    return v3
+}
+
+; VCode:
+;   pushq   %rbp
+;   movq    %rsp, %rbp
+; block0:
+;   pinsrb  $3, %xmm0, 0(%rdi), %xmm0
+;   movq    %rbp, %rsp
+;   popq    %rbp
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   pushq %rbp
+;   movq %rsp, %rbp
+; block1: ; offset 0x4
+;   pinsrb $3, (%rdi), %xmm0 ; trap: heap_oob
+;   movq %rbp, %rsp
+;   popq %rbp
+;   retq
+
+function %store32_lane_coalesced(i64, i32x4) {
+block0(v0: i64, v1: i32x4):
+    v2 = extractlane.i32x4 v1, 3
+    store.i32 v2, v0
+    return
+}
+
+; VCode:
+;   pushq   %rbp
+;   movq    %rsp, %rbp
+; block0:
+;   pextrd  $3, %xmm0, 0(%rdi)
+;   movq    %rbp, %rsp
+;   popq    %rbp
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   pushq %rbp
+;   movq %rsp, %rbp
+; block1: ; offset 0x4
+;   pextrd $3, %xmm0, (%rdi) ; trap: heap_oob
+;   movq %rbp, %rsp
+;   popq %rbp
+;   retq
+
+function %store8_lane_coalesced(i64, i16x8) {
+block0(v0: i64, v1: i16x8):
+    v2 = extractlane.i16x8 v1, 3
+    store.i16 v2, v0
+    return
+}
+
+; VCode:
+;   pushq   %rbp
+;   movq    %rsp, %rbp
+; block0:
+;   pextrw  $3, %xmm0, 0(%rdi)
+;   movq    %rbp, %rsp
+;   popq    %rbp
+;   ret
+;
+; Disassembled:
+; block0: ; offset 0x0
+;   pushq %rbp
+;   movq %rsp, %rbp
+; block1: ; offset 0x4
+;   pextrw $3, %xmm0, (%rdi) ; trap: heap_oob
+;   movq %rbp, %rsp
+;   popq %rbp
+;   retq
+
diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs
index 8fa270135..146f1dd72 100644
--- a/tests/all/cli_tests.rs
+++ b/tests/all/cli_tests.rs
@@ -1499,7 +1499,7 @@ mod test_programs {
             &[
                 "-Ccache=no",
                 "-Wcomponent-model",
-                "-Scommon,http,preview2",
+                "-Scli,http,preview2",
                 HTTP_OUTBOUND_REQUEST_RESPONSE_BUILD_COMPONENT,
             ],
             None,
diff --git a/tests/disas/typed-funcrefs.wat b/tests/disas/typed-funcrefs.wat
index 78eb51b3b..07f52e6aa 100644
--- a/tests/disas/typed-funcrefs.wat
+++ b/tests/disas/typed-funcrefs.wat
@@ -21,7 +21,8 @@
   (func $ic1 (param i32 i32 i32 i32) (result i32)
         local.get 0)
 
-  (func $call-ics (param i32 i32 i32 i32) (result i32)
+  ;; A function which uses ICs through `table.get` plus `call_ref`
+  (func $call-ics-with-call-ref (param i32 i32 i32 i32) (result i32)
         (local $sum i32)
 
         ;; IC callsite index 1 (arbitrary).
@@ -48,7 +49,69 @@
         i32.add
         local.set $sum
 
-        local.get $sum))
+        local.get $sum)
+
+  ;; Same as the above function, but uses `call_indirect` rather than
+  ;; `call_ref`.
+  (func $call-ics-with-call-indirect (param i32 i32 i32 i32) (result i32)
+        (local $sum i32)
+
+        ;; IC callsite index 1 (arbitrary).
+        local.get 0
+        local.get 1
+        local.get 2
+        local.get 3
+        i32.const 1
+        call_indirect $ic-sites (type $ic-stub)
+        local.get $sum
+        i32.add
+        local.set $sum
+
+        ;; IC callsite index 2 (arbitrary).
+        local.get 0
+        local.get 1
+        local.get 2
+        local.get 3
+        i32.const 2
+        call_indirect $ic-sites (type $ic-stub)
+        local.get $sum
+        i32.add
+        local.set $sum
+
+        local.get $sum)
+
+  (global $ic-site0 (mut (ref $ic-stub)) (ref.func $ic1))
+  (global $ic-site1 (mut (ref $ic-stub)) (ref.func $ic1))
+
+  ;; Sort of similar to the previous two functions, but uses globals instead of
+  ;; tables to store ICs. Mostly just here for comparison in terms of codegen.
+  (func $call-ics-with-global-get (param i32 i32 i32 i32) (result i32)
+        (local $sum i32)
+
+        ;; IC callsite index 1 (arbitrary).
+        local.get 0
+        local.get 1
+        local.get 2
+        local.get 3
+        global.get $ic-site0
+        call_ref $ic-stub
+        local.get $sum
+        i32.add
+        local.set $sum
+
+        ;; IC callsite index 2 (arbitrary).
+        local.get 0
+        local.get 1
+        local.get 2
+        local.get 3
+        global.get $ic-site1
+        call_ref $ic-stub
+        local.get $sum
+        i32.add
+        local.set $sum
+
+        local.get $sum)
+)
 ;; function u0:0(i64 vmctx, i64, i32, i32, i32, i32) -> i32 fast {
 ;;     gv0 = vmctx
 ;;     gv1 = load.i64 notrap aligned readonly gv0+8
@@ -59,10 +122,10 @@
 ;;
 ;;                                 block0(v0: i64, v1: i64, v2: i32, v3: i32, v4: i32, v5: i32):
 ;;                                     v6 -> v2
-;; @0027                               jump block1
+;; @0039                               jump block1
 ;;
 ;;                                 block1:
-;; @0027                               return v2
+;; @0039                               return v2
 ;; }
 ;;
 ;; function u0:1(i64 vmctx, i64, i32, i32, i32, i32) -> i32 fast {
@@ -87,68 +150,185 @@
 ;;                                     v32 -> v4
 ;;                                     v33 -> v5
 ;;                                     v62 = iconst.i8 0
-;; @0036                               brif v62, block6, block7  ; v62 = 0
+;; @0048                               brif v62, block6, block7  ; v62 = 0
+;;
+;;                                 block6 cold:
+;; @0048                               trap table_oob
+;;
+;;                                 block7:
+;; @0048                               v12 = load.i64 notrap aligned v0+72
+;;                                     v79 = iconst.i8 0
+;;                                     v70 = iconst.i64 8
+;; @0048                               v14 = iadd v12, v70  ; v70 = 8
+;; @0048                               v16 = select_spectre_guard v79, v12, v14  ; v79 = 0
+;; @0048                               v17 = load.i64 notrap aligned table v16
+;;                                     v58 = iconst.i64 -2
+;; @0048                               v18 = band v17, v58  ; v58 = -2
+;; @0048                               brif v17, block3(v18), block2
+;;
+;;                                 block2 cold:
+;; @005b                               v48 = load.i64 notrap aligned readonly v0+56
+;; @005b                               v49 = load.i64 notrap aligned readonly v48+72
+;; @003c                               v7 = iconst.i32 0
+;;                                     v28 -> v7
+;; @0046                               v8 = iconst.i32 1
+;; @0048                               v24 = call_indirect sig0, v49(v0, v7, v8)  ; v7 = 0, v8 = 1
+;; @0048                               jump block3(v24)
+;;
+;;                                 block3(v19: i64):
+;; @004a                               v25 = load.i64 null_reference aligned readonly v19+16
+;; @004a                               v26 = load.i64 notrap aligned readonly v19+32
+;; @004a                               v27 = call_indirect sig1, v25(v26, v0, v2, v3, v4, v5)
+;;                                     v80 = iconst.i8 0
+;; @005b                               brif v80, block8, block9  ; v80 = 0
+;;
+;;                                 block8 cold:
+;; @005b                               trap table_oob
+;;
+;;                                 block9:
+;; @005b                               v38 = load.i64 notrap aligned v0+72
+;;                                     v81 = iconst.i8 0
+;;                                     v78 = iconst.i64 16
+;; @005b                               v40 = iadd v38, v78  ; v78 = 16
+;; @005b                               v42 = select_spectre_guard v81, v38, v40  ; v81 = 0
+;; @005b                               v43 = load.i64 notrap aligned table v42
+;;                                     v82 = iconst.i64 -2
+;;                                     v83 = band v43, v82  ; v82 = -2
+;; @005b                               brif v43, block5(v83), block4
+;;
+;;                                 block4 cold:
+;;                                     v84 = load.i64 notrap aligned readonly v0+56
+;;                                     v85 = load.i64 notrap aligned readonly v84+72
+;;                                     v86 = iconst.i32 0
+;; @0059                               v34 = iconst.i32 2
+;; @005b                               v50 = call_indirect sig0, v85(v0, v86, v34)  ; v86 = 0, v34 = 2
+;; @005b                               jump block5(v50)
+;;
+;;                                 block5(v45: i64):
+;; @005d                               v51 = load.i64 null_reference aligned readonly v45+16
+;; @005d                               v52 = load.i64 notrap aligned readonly v45+32
+;; @005d                               v53 = call_indirect sig1, v51(v52, v0, v2, v3, v4, v5)
+;; @0066                               jump block1
+;;
+;;                                 block1:
+;; @0061                               v55 = iadd.i32 v53, v27
+;;                                     v6 -> v55
+;; @0066                               return v55
+;; }
+;;
+;; function u0:2(i64 vmctx, i64, i32, i32, i32, i32) -> i32 fast {
+;;     gv0 = vmctx
+;;     gv1 = load.i64 notrap aligned readonly gv0+8
+;;     gv2 = load.i64 notrap aligned gv1
+;;     gv3 = vmctx
+;;     gv4 = load.i64 notrap aligned gv3+72
+;;     sig0 = (i64 vmctx, i64, i32, i32, i32, i32) -> i32 fast
+;;     sig1 = (i64 vmctx, i32 uext, i32 uext) -> i64 system_v
+;;     sig2 = (i64 vmctx, i32 uext, i32 uext) -> i32 uext system_v
+;;     sig3 = (i64 vmctx, i32 uext) -> i32 uext system_v
+;;     stack_limit = gv2
+;;
+;;                                 block0(v0: i64, v1: i64, v2: i32, v3: i32, v4: i32, v5: i32):
+;;                                     v21 -> v0
+;;                                     v47 -> v0
+;;                                     v56 -> v0
+;;                                     v59 -> v0
+;;                                     v30 -> v2
+;;                                     v31 -> v3
+;;                                     v32 -> v4
+;;                                     v33 -> v5
+;;                                     v62 = iconst.i8 0
+;; @0075                               brif v62, block6, block7  ; v62 = 0
 ;;
 ;;                                 block6 cold:
-;; @0036                               trap table_oob
+;; @0075                               trap table_oob
 ;;
 ;;                                 block7:
-;; @0036                               v12 = load.i64 notrap aligned v0+72
+;; @0075                               v12 = load.i64 notrap aligned v0+72
 ;;                                     v79 = iconst.i8 0
 ;;                                     v70 = iconst.i64 8
-;; @0036                               v14 = iadd v12, v70  ; v70 = 8
-;; @0036                               v16 = select_spectre_guard v79, v12, v14  ; v79 = 0
-;; @0036                               v17 = load.i64 notrap aligned table v16
+;; @0075                               v14 = iadd v12, v70  ; v70 = 8
+;; @0075                               v16 = select_spectre_guard v79, v12, v14  ; v79 = 0
+;; @0075                               v17 = load.i64 notrap aligned table v16
 ;;                                     v58 = iconst.i64 -2
-;; @0036                               v18 = band v17, v58  ; v58 = -2
-;; @0036                               brif v17, block3(v18), block2
+;; @0075                               v18 = band v17, v58  ; v58 = -2
+;; @0075                               brif v17, block3(v18), block2
 ;;
 ;;                                 block2 cold:
-;; @0049                               v48 = load.i64 notrap aligned readonly v0+56
-;; @0049                               v49 = load.i64 notrap aligned readonly v48+72
-;; @002a                               v7 = iconst.i32 0
+;; @0087                               v48 = load.i64 notrap aligned readonly v0+56
+;; @0087                               v49 = load.i64 notrap aligned readonly v48+72
+;; @0069                               v7 = iconst.i32 0
 ;;                                     v28 -> v7
-;; @0034                               v8 = iconst.i32 1
-;; @0036                               v24 = call_indirect sig0, v49(v0, v7, v8)  ; v7 = 0, v8 = 1
-;; @0036                               jump block3(v24)
+;; @0073                               v8 = iconst.i32 1
+;; @0075                               v24 = call_indirect sig1, v49(v0, v7, v8)  ; v7 = 0, v8 = 1
+;; @0075                               jump block3(v24)
 ;;
 ;;                                 block3(v19: i64):
-;; @0038                               v25 = load.i64 null_reference aligned readonly v19+16
-;; @0038                               v26 = load.i64 notrap aligned readonly v19+32
-;; @0038                               v27 = call_indirect sig1, v25(v26, v0, v2, v3, v4, v5)
+;; @0075                               v25 = load.i64 icall_null aligned readonly v19+16
+;; @0075                               v26 = load.i64 notrap aligned readonly v19+32
+;; @0075                               v27 = call_indirect sig0, v25(v26, v0, v2, v3, v4, v5)
 ;;                                     v80 = iconst.i8 0
-;; @0049                               brif v80, block8, block9  ; v80 = 0
+;; @0087                               brif v80, block8, block9  ; v80 = 0
 ;;
 ;;                                 block8 cold:
-;; @0049                               trap table_oob
+;; @0087                               trap table_oob
 ;;
 ;;                                 block9:
-;; @0049                               v38 = load.i64 notrap aligned v0+72
+;; @0087                               v38 = load.i64 notrap aligned v0+72
 ;;                                     v81 = iconst.i8 0
 ;;                                     v78 = iconst.i64 16
-;; @0049                               v40 = iadd v38, v78  ; v78 = 16
-;; @0049                               v42 = select_spectre_guard v81, v38, v40  ; v81 = 0
-;; @0049                               v43 = load.i64 notrap aligned table v42
+;; @0087                               v40 = iadd v38, v78  ; v78 = 16
+;; @0087                               v42 = select_spectre_guard v81, v38, v40  ; v81 = 0
+;; @0087                               v43 = load.i64 notrap aligned table v42
 ;;                                     v82 = iconst.i64 -2
 ;;                                     v83 = band v43, v82  ; v82 = -2
-;; @0049                               brif v43, block5(v83), block4
+;; @0087                               brif v43, block5(v83), block4
 ;;
 ;;                                 block4 cold:
 ;;                                     v84 = load.i64 notrap aligned readonly v0+56
 ;;                                     v85 = load.i64 notrap aligned readonly v84+72
 ;;                                     v86 = iconst.i32 0
-;; @0047                               v34 = iconst.i32 2
-;; @0049                               v50 = call_indirect sig0, v85(v0, v86, v34)  ; v86 = 0, v34 = 2
-;; @0049                               jump block5(v50)
+;; @0085                               v34 = iconst.i32 2
+;; @0087                               v50 = call_indirect sig1, v85(v0, v86, v34)  ; v86 = 0, v34 = 2
+;; @0087                               jump block5(v50)
 ;;
 ;;                                 block5(v45: i64):
-;; @004b                               v51 = load.i64 null_reference aligned readonly v45+16
-;; @004b                               v52 = load.i64 notrap aligned readonly v45+32
-;; @004b                               v53 = call_indirect sig1, v51(v52, v0, v2, v3, v4, v5)
-;; @0054                               jump block1
+;; @0087                               v51 = load.i64 icall_null aligned readonly v45+16
+;; @0087                               v52 = load.i64 notrap aligned readonly v45+32
+;; @0087                               v53 = call_indirect sig0, v51(v52, v0, v2, v3, v4, v5)
+;; @0091                               jump block1
 ;;
 ;;                                 block1:
-;; @004f                               v55 = iadd.i32 v53, v27
+;; @008c                               v55 = iadd.i32 v53, v27
 ;;                                     v6 -> v55
-;; @0054                               return v55
+;; @0091                               return v55
+;; }
+;;
+;; function u0:3(i64 vmctx, i64, i32, i32, i32, i32) -> i32 fast {
+;;     gv0 = vmctx
+;;     gv1 = load.i64 notrap aligned readonly gv0+8
+;;     gv2 = load.i64 notrap aligned gv1
+;;     gv3 = vmctx
+;;     sig0 = (i64 vmctx, i64, i32, i32, i32, i32) -> i32 fast
+;;     sig1 = (i64 vmctx, i32 uext, i32 uext) -> i32 uext system_v
+;;     sig2 = (i64 vmctx, i32 uext) -> i32 uext system_v
+;;     stack_limit = gv2
+;;
+;;                                 block0(v0: i64, v1: i64, v2: i32, v3: i32, v4: i32, v5: i32):
+;;                                     v8 -> v0
+;;                                     v14 -> v0
+;; @009e                               v9 = load.i64 notrap aligned table v0+96
+;; @00a0                               v10 = load.i64 null_reference aligned readonly v9+16
+;; @00a0                               v11 = load.i64 notrap aligned readonly v9+32
+;; @00a0                               v12 = call_indirect sig0, v10(v11, v0, v2, v3, v4, v5)
+;; @00af                               v15 = load.i64 notrap aligned table v0+112
+;; @00b1                               v16 = load.i64 null_reference aligned readonly v15+16
+;; @00b1                               v17 = load.i64 notrap aligned readonly v15+32
+;; @00b1                               v18 = call_indirect sig0, v16(v17, v0, v2, v3, v4, v5)
+;; @00ba                               jump block1
+;;
+;;                                 block1:
+;; @00b5                               v19 = iadd.i32 v18, v12
+;;                                     v6 -> v19
+;; @00ba                               return v19
 ;; }
diff --git a/tests/misc_testsuite/function-references/call_indirect.wast b/tests/misc_testsuite/function-references/call_indirect.wast
new file mode 100644
index 000000000..60fabf9c6
--- /dev/null
+++ b/tests/misc_testsuite/function-references/call_indirect.wast
@@ -0,0 +1,54 @@
+(module
+  (table $t1 2 funcref)
+  (elem (table $t1) (i32.const 0) func $nop)
+  (func $nop)
+
+  (func (export "t1") (param i32)
+    local.get 0
+    call_indirect $t1)
+  (func (export "t1-wrong-type") (param i32)
+    i32.const 0
+    local.get 0
+    call_indirect $t1 (param i32))
+
+  (type $empty (func))
+  (table $t2 2 (ref null $empty))
+  (elem (table $t2) (i32.const 0) (ref null $empty) (ref.func $nop))
+
+  (func (export "t2") (param i32)
+    local.get 0
+    call_indirect $t2)
+  (func (export "t2-wrong-type") (param i32)
+    i32.const 0
+    local.get 0
+    call_indirect $t2 (param i32))
+
+  (table $t3 2 (ref $empty) (ref.func $nop))
+
+  (func (export "t3") (param i32)
+    local.get 0
+    call_indirect $t3)
+  (func (export "t3-wrong-type") (param i32)
+    i32.const 0
+    local.get 0
+    call_indirect $t3 (param i32))
+)
+
+(assert_return (invoke "t1" (i32.const 0)))
+(assert_trap (invoke "t1" (i32.const 1)) "uninitialized element")
+(assert_trap (invoke "t1" (i32.const 2)) "out of bounds")
+(assert_trap (invoke "t1-wrong-type" (i32.const 0)) "call type mismatch")
+(assert_trap (invoke "t1-wrong-type" (i32.const 1)) "uninitialized element")
+(assert_trap (invoke "t1-wrong-type" (i32.const 2)) "out of bounds")
+(assert_return (invoke "t2" (i32.const 0)))
+(assert_trap (invoke "t2" (i32.const 1)) "uninitialized element")
+(assert_trap (invoke "t2" (i32.const 2)) "out of bounds")
+(assert_trap (invoke "t2-wrong-type" (i32.const 0)) "call type mismatch")
+(assert_trap (invoke "t2-wrong-type" (i32.const 1)) "uninitialized element")
+(assert_trap (invoke "t2-wrong-type" (i32.const 2)) "out of bounds")
+(assert_return (invoke "t3" (i32.const 0)))
+(assert_return (invoke "t3" (i32.const 1)))
+(assert_trap (invoke "t3" (i32.const 2)) "out of bounds")
+(assert_trap (invoke "t3-wrong-type" (i32.const 0)) "call type mismatch")
+(assert_trap (invoke "t3-wrong-type" (i32.const 1)) "call type mismatch")
+(assert_trap (invoke "t3-wrong-type" (i32.const 2)) "out of bounds")