Refreshing VM performance by separating verification and tweaking buffer access. (#12426)

This moves much of the interpreter checks to an ahead-of-time bytecode
verifier. This allows us to share the same verification with JITs and
disable it entirely for code size reasons using the
`-DIREE_VM_BYTECODE_VERIFICATION_ENABLE=0` compiler flag. Verification
is pretty exhaustive but may still need some additions. It's
significantly better than before, though, so even if not the final form
it's a good step. Simpler verification (and dispatch) will come with a
pending bytecode shuffling into instruction classes.

Since this required breaking binary compatibility I did some deferred
changes that had been sitting in
https://github.com/openxla/iree/projects/32, namely requirement bits and
fixing vm.buffer.fill from bytes to elements.

The requirements give us much nicer error messages by supporting
per-module and per-function bitfields indicating features required to
execute the bytecode they contain, e.g.:
```
D:\Dev\iree\runtime\src\iree\vm\bytecode\module.c:309: INVALID_ARGUMENT; required module features [EXT_F32] are not available in this runtime configuration; have [] while module requires [EXT_F32]; while invoking native function hal.executable.create; while calling import;
[ 1]   native hal.executable.create:0 -
[ 0] bytecode module.__init:446 D:\Dev\iree/tests/e2e/models/unidirectional_lstm.mlir:0:0
```

Splitting out the verification from dispatch is good for code
reuse/optionality but also now lets us dispatch without verifying.
Between removing the inlined verification/register masking/etc and
streamlining buffer access we get a near 2x speedup of compute-heavy
VMVX workloads. resnet50 for example on my ryzen system (with
`--iree-vm-target-index-bits=64`, which I need to make default):

```
before:
1 core:  BM_predict/process_time/real_time     343774 ms       343766 ms            1 items_per_second=2.90888m/s
8 core:  BM_predict/process_time/real_time      48306 ms       361156 ms            1 items_per_second=0.0207012/s
32 core: BM_predict/process_time/real_time      18943 ms       408922 ms            2 items_per_second=0.0527891/s

after:
1 core:  BM_predict/process_time/real_time     147856 ms       147859 ms            1 items_per_second=6.76332m/s
8 core:  BM_predict/process_time/real_time      21569 ms       158781 ms            1 items_per_second=0.0463637/s
32 core: BM_predict/process_time/real_time       8962 ms       186276 ms            3 items_per_second=0.111579/s
```

About ~20-30% of the remaining runtime is spent in bytecode dispatch
which needs a larger op table reworking to make better. That'll also
reduce code size quite a bit as today we have a lot of duplicate
decoding work. The most expensive ops remaining are buffer loads/stores
and short of JIT or scatter/gather such as #8477 there's not much to do
besides less work. Today codegen is producing some phenomenally bad code
and we're executing ~100x+ more instructions than required
(https://gist.github.com/benvanik/e2b45891e02baf8318109b60189a1b12 for
example - that's _a lot_ of loop arithmetic, a useless fill that should
be removed or at least turned into a util.buffer.fill, and unfused
writebacks) - even without op table shuffling or microkernels we should
be well under 900ms instead of 9000ms. We've also got to parameterize
our workgroup distribution - today we don't tend to use more than 4-16
cores so we don't see the latency improvement we'd expect going 8->32
cores (16 cores is nearly identical).

These issues also impact emitc paths as the C compiler downstream of
that is dealing with all this difficult to analyze output and can't do
much. I thought an emitc resnet with inline VMVX would be a good
approximation of what a JIT could do and it's not good: 448154ms vs the
147856ms of the interpreter! It's also 2x larger in size on disk (380KB
x86_64 vs 200KB bytecode) and that's prior to optimization of the
bytecode. We should definitely be able to do 2-4x faster with a naïve
JIT - if not 10x!

Fixes #5732.
Fixes #12373.
diff --git a/build_tools/cmake/iree_copts.cmake b/build_tools/cmake/iree_copts.cmake
index 4e16665..1956b04 100644
--- a/build_tools/cmake/iree_copts.cmake
+++ b/build_tools/cmake/iree_copts.cmake
@@ -402,6 +402,7 @@
       "-DIREE_HAL_MODULE_STRING_UTIL_ENABLE=0"
       "-DIREE_HAL_COMMAND_BUFFER_VALIDATION_ENABLE=0"
       "-DIREE_VM_BACKTRACE_ENABLE=0"
+      "-DIREE_VM_BYTECODE_VERIFICATION_ENABLE=0"
       "-DIREE_VM_EXT_F32_ENABLE=0"
       "-DIREE_VM_EXT_F64_ENABLE=0"
   )
diff --git a/compiler/src/iree/compiler/API2/Internal/Embed.cpp b/compiler/src/iree/compiler/API2/Internal/Embed.cpp
index dcf0a1b..ca3bc23 100644
--- a/compiler/src/iree/compiler/API2/Internal/Embed.cpp
+++ b/compiler/src/iree/compiler/API2/Internal/Embed.cpp
@@ -472,11 +472,13 @@
   auto builtinModule = llvm::dyn_cast<mlir::ModuleOp>(*parsedModule);
   LogicalResult result = failure();
   if (vmModule) {
-    result = translateModuleToBytecode(vmModule, session.bytecodeTargetOptions,
+    result = translateModuleToBytecode(vmModule, session.vmTargetOptions,
+                                       session.bytecodeTargetOptions,
                                        *output.outputStream);
   } else if (builtinModule) {
-    result = translateModuleToBytecode(
-        builtinModule, session.bytecodeTargetOptions, *output.outputStream);
+    result = translateModuleToBytecode(builtinModule, session.vmTargetOptions,
+                                       session.bytecodeTargetOptions,
+                                       *output.outputStream);
   } else {
     (*parsedModule)->emitError() << "expected a vm.module or builtin.module";
   }
diff --git a/compiler/src/iree/compiler/API2/Internal/MLIRInterop.cpp b/compiler/src/iree/compiler/API2/Internal/MLIRInterop.cpp
index 95317b3..ad09f51 100644
--- a/compiler/src/iree/compiler/API2/Internal/MLIRInterop.cpp
+++ b/compiler/src/iree/compiler/API2/Internal/MLIRInterop.cpp
@@ -137,11 +137,13 @@
   mlir::detail::CallbackOstream output(dataCallback, dataUserObject);
   if (auto op = llvm::dyn_cast<mlir::ModuleOp>(moduleOpCpp)) {
     result = iree_compiler::IREE::VM::translateModuleToBytecode(
-        op, optionsCpp->vmBytecodeTargetOptions, output);
+        op, optionsCpp->vmTargetOptions, optionsCpp->vmBytecodeTargetOptions,
+        output);
   } else if (auto op = llvm::dyn_cast<iree_compiler::IREE::VM::ModuleOp>(
                  moduleOpCpp)) {
     result = iree_compiler::IREE::VM::translateModuleToBytecode(
-        op, optionsCpp->vmBytecodeTargetOptions, output);
+        op, optionsCpp->vmTargetOptions, optionsCpp->vmBytecodeTargetOptions,
+        output);
   } else {
     emitError(moduleOpCpp->getLoc()) << "expected a supported module operation";
     result = failure();
diff --git a/compiler/src/iree/compiler/ConstEval/Runtime.cpp b/compiler/src/iree/compiler/ConstEval/Runtime.cpp
index 51d191f..d14198a 100644
--- a/compiler/src/iree/compiler/ConstEval/Runtime.cpp
+++ b/compiler/src/iree/compiler/ConstEval/Runtime.cpp
@@ -295,9 +295,10 @@
 LogicalResult InMemoryCompiledBinary::translateFromModule(
     mlir::ModuleOp moduleOp) {
   llvm::raw_string_ostream os(binary);
-  iree_compiler::IREE::VM::BytecodeTargetOptions targetOptions;
+  iree_compiler::IREE::VM::TargetOptions vmOptions;
+  iree_compiler::IREE::VM::BytecodeTargetOptions bytecodeOptions;
   if (failed(iree_compiler::IREE::VM::translateModuleToBytecode(
-          moduleOp, targetOptions, os))) {
+          moduleOp, vmOptions, bytecodeOptions, os))) {
     return failure();
   }
   os.flush();
diff --git a/compiler/src/iree/compiler/Dialect/HAL/Target/VMVX/VMVXTarget.cpp b/compiler/src/iree/compiler/Dialect/HAL/Target/VMVX/VMVXTarget.cpp
index 031e7d4..c701689 100644
--- a/compiler/src/iree/compiler/Dialect/HAL/Target/VMVX/VMVXTarget.cpp
+++ b/compiler/src/iree/compiler/Dialect/HAL/Target/VMVX/VMVXTarget.cpp
@@ -70,16 +70,22 @@
         context, b.getStringAttr(deviceID()), configAttr);
   }
 
+  IREE::VM::TargetOptions getTargetOptions(
+      IREE::HAL::ExecutableTargetAttr targetAttr) {
+    // TODO(benvanik): derive these from a vm target triple.
+    auto vmOptions = IREE::VM::TargetOptions::FromFlags::get();
+    vmOptions.f32Extension = true;
+    vmOptions.optimizeForStackSize = false;
+    return vmOptions;
+  }
+
   void buildTranslationPassPipeline(IREE::HAL::ExecutableVariantOp variantOp,
                                     OpPassManager &passManager) override {
     IREE::VMVX::buildVMVXTransformPassPipeline(passManager);
 
     OpPassManager &nestedModulePM = passManager.nest<ModuleOp>();
 
-    // TODO(benvanik): derive these from a vm target triple.
-    auto vmOptions = IREE::VM::TargetOptions::FromFlags::get();
-    vmOptions.f32Extension = true;
-    vmOptions.optimizeForStackSize = false;
+    auto vmOptions = getTargetOptions(variantOp.getTargetAttr());
     IREE::VM::buildVMTransformPassPipeline(nestedModulePM, vmOptions);
   }
 
@@ -107,10 +113,15 @@
     // Serialize the VM module to bytes and embed it directly.
     SmallVector<char> moduleData;
     {
-      IREE::VM::BytecodeTargetOptions bytecodeOptions;
+      auto vmOptions = getTargetOptions(variantOp.getTargetAttr());
+      // TODO(benvanik): plumb this through somewhere? these options are mostly
+      // about output format stuff such as debug information so it's probably
+      // fine to share.
+      auto bytecodeOptions = IREE::VM::BytecodeTargetOptions::FromFlags::get();
       llvm::raw_svector_ostream stream(moduleData);
       if (failed(translateModuleToBytecode(variantOp.getInnerModule(),
-                                           bytecodeOptions, stream))) {
+                                           vmOptions, bytecodeOptions,
+                                           stream))) {
         return variantOp.emitOpError()
                << "failed to serialize VM bytecode module";
       }
diff --git a/compiler/src/iree/compiler/Dialect/VM/Conversion/UtilToVM/ConvertBufferOps.cpp b/compiler/src/iree/compiler/Dialect/VM/Conversion/UtilToVM/ConvertBufferOps.cpp
index 73bd4b0..4e07f63 100644
--- a/compiler/src/iree/compiler/Dialect/VM/Conversion/UtilToVM/ConvertBufferOps.cpp
+++ b/compiler/src/iree/compiler/Dialect/VM/Conversion/UtilToVM/ConvertBufferOps.cpp
@@ -154,6 +154,14 @@
   }
 };
 
+static Value unscaleOffset(Location loc, Value offset, int64_t scale,
+                           OpBuilder &builder) {
+  if (scale == 1) return offset;
+  return builder.createOrFold<IREE::VM::DivI64SOp>(
+      loc, offset.getType(), offset,
+      builder.create<IREE::VM::ConstI64Op>(loc, scale));
+}
+
 struct BufferFillOpConversion
     : public OpConversionPattern<IREE::Util::BufferFillOp> {
   using OpConversionPattern::OpConversionPattern;
@@ -168,6 +176,11 @@
     }
     auto byteOffset = castToI64(adaptor.getTargetOffset(), rewriter);
     auto byteLength = castToI64(adaptor.getLength(), rewriter);
+    int64_t elementSize = IREE::Util::getRoundedElementByteWidth(oldType);
+    auto elementOffset =
+        unscaleOffset(fillOp.getLoc(), byteOffset, elementSize, rewriter);
+    auto elementLength =
+        unscaleOffset(fillOp.getLoc(), byteLength, elementSize, rewriter);
     auto pattern = adaptor.getPattern();
     if (auto integerType = oldType.dyn_cast<IntegerType>()) {
       if (integerType.isInteger(1) || integerType.isInteger(8)) {
@@ -175,23 +188,23 @@
             fillOp, adaptor.getTarget(), byteOffset, byteLength, pattern);
       } else if (integerType.isInteger(16)) {
         rewriter.replaceOpWithNewOp<IREE::VM::BufferFillI16Op>(
-            fillOp, adaptor.getTarget(), byteOffset, byteLength, pattern);
+            fillOp, adaptor.getTarget(), elementOffset, elementLength, pattern);
       } else if (integerType.isInteger(32)) {
         rewriter.replaceOpWithNewOp<IREE::VM::BufferFillI32Op>(
-            fillOp, adaptor.getTarget(), byteOffset, byteLength, pattern);
+            fillOp, adaptor.getTarget(), elementOffset, elementLength, pattern);
       } else if (integerType.isInteger(64)) {
         rewriter.replaceOpWithNewOp<IREE::VM::BufferFillI64Op>(
-            fillOp, adaptor.getTarget(), byteOffset, byteLength, pattern);
+            fillOp, adaptor.getTarget(), elementOffset, elementLength, pattern);
       } else {
         return rewriter.notifyMatchFailure(
             fillOp, "invalid integer buffer element type");
       }
     } else if (oldType.isF32()) {
       rewriter.replaceOpWithNewOp<IREE::VM::BufferFillF32Op>(
-          fillOp, adaptor.getTarget(), byteOffset, byteLength, pattern);
+          fillOp, adaptor.getTarget(), elementOffset, elementLength, pattern);
     } else if (oldType.isF64()) {
       rewriter.replaceOpWithNewOp<IREE::VM::BufferFillF64Op>(
-          fillOp, adaptor.getTarget(), byteOffset, byteLength, pattern);
+          fillOp, adaptor.getTarget(), elementOffset, elementLength, pattern);
     } else {
       return rewriter.notifyMatchFailure(fillOp,
                                          "invalid float buffer element type");
@@ -200,14 +213,6 @@
   }
 };
 
-static Value unscaleOffset(Location loc, Value offset, int64_t scale,
-                           OpBuilder &builder) {
-  if (scale == 1) return offset;
-  return builder.createOrFold<IREE::VM::DivI64SOp>(
-      loc, offset.getType(), offset,
-      builder.create<IREE::VM::ConstI64Op>(loc, scale));
-}
-
 struct BufferLoadOpConversion
     : public OpConversionPattern<IREE::Util::BufferLoadOp> {
   using OpConversionPattern::OpConversionPattern;
@@ -220,6 +225,9 @@
       oldType = newType;
     }
     auto byteOffset = castToI64(adaptor.getSourceOffset(), rewriter);
+    int64_t elementSize = IREE::Util::getRoundedElementByteWidth(oldType);
+    auto elementOffset =
+        unscaleOffset(loadOp.getLoc(), byteOffset, elementSize, rewriter);
     if (auto integerType = oldType.dyn_cast<IntegerType>()) {
       if (integerType.isInteger(1) || integerType.isInteger(8)) {
         if (integerType.isSigned() || integerType.isSignless()) {
@@ -232,33 +240,27 @@
       } else if (integerType.isInteger(16)) {
         if (integerType.isSigned() || integerType.isSignless()) {
           rewriter.replaceOpWithNewOp<IREE::VM::BufferLoadI16SOp>(
-              loadOp, newType, adaptor.getSource(),
-              unscaleOffset(loadOp.getLoc(), byteOffset, 2, rewriter));
+              loadOp, newType, adaptor.getSource(), elementOffset);
         } else {
           rewriter.replaceOpWithNewOp<IREE::VM::BufferLoadI16UOp>(
-              loadOp, newType, adaptor.getSource(),
-              unscaleOffset(loadOp.getLoc(), byteOffset, 2, rewriter));
+              loadOp, newType, adaptor.getSource(), elementOffset);
         }
       } else if (integerType.isInteger(32)) {
         rewriter.replaceOpWithNewOp<IREE::VM::BufferLoadI32Op>(
-            loadOp, newType, adaptor.getSource(),
-            unscaleOffset(loadOp.getLoc(), byteOffset, 4, rewriter));
+            loadOp, newType, adaptor.getSource(), elementOffset);
       } else if (integerType.isInteger(64)) {
         rewriter.replaceOpWithNewOp<IREE::VM::BufferLoadI64Op>(
-            loadOp, newType, adaptor.getSource(),
-            unscaleOffset(loadOp.getLoc(), byteOffset, 8, rewriter));
+            loadOp, newType, adaptor.getSource(), elementOffset);
       } else {
         return rewriter.notifyMatchFailure(
             loadOp, "invalid integer buffer element type");
       }
     } else if (oldType.isF32()) {
       rewriter.replaceOpWithNewOp<IREE::VM::BufferLoadF32Op>(
-          loadOp, newType, adaptor.getSource(),
-          unscaleOffset(loadOp.getLoc(), byteOffset, 4, rewriter));
+          loadOp, newType, adaptor.getSource(), elementOffset);
     } else if (oldType.isF64()) {
       rewriter.replaceOpWithNewOp<IREE::VM::BufferLoadF64Op>(
-          loadOp, newType, adaptor.getSource(),
-          unscaleOffset(loadOp.getLoc(), byteOffset, 8, rewriter));
+          loadOp, newType, adaptor.getSource(), elementOffset);
     } else {
       return rewriter.notifyMatchFailure(loadOp,
                                          "invalid float buffer element type");
@@ -279,34 +281,27 @@
       oldType = newType;
     }
     auto byteOffset = castToI64(adaptor.getTargetOffset(), rewriter);
+    int64_t elementSize = IREE::Util::getRoundedElementByteWidth(oldType);
+    auto elementOffset =
+        unscaleOffset(storeOp.getLoc(), byteOffset, elementSize, rewriter);
     if (oldType.isInteger(1) || oldType.isInteger(8)) {
       rewriter.replaceOpWithNewOp<IREE::VM::BufferStoreI8Op>(
           storeOp, adaptor.getTarget(), byteOffset, adaptor.getSource());
     } else if (oldType.isInteger(16)) {
       rewriter.replaceOpWithNewOp<IREE::VM::BufferStoreI16Op>(
-          storeOp, adaptor.getTarget(),
-          unscaleOffset(storeOp.getLoc(), byteOffset, 2, rewriter),
-          adaptor.getSource());
+          storeOp, adaptor.getTarget(), elementOffset, adaptor.getSource());
     } else if (oldType.isInteger(32)) {
       rewriter.replaceOpWithNewOp<IREE::VM::BufferStoreI32Op>(
-          storeOp, adaptor.getTarget(),
-          unscaleOffset(storeOp.getLoc(), byteOffset, 4, rewriter),
-          adaptor.getSource());
+          storeOp, adaptor.getTarget(), elementOffset, adaptor.getSource());
     } else if (oldType.isInteger(64)) {
       rewriter.replaceOpWithNewOp<IREE::VM::BufferStoreI64Op>(
-          storeOp, adaptor.getTarget(),
-          unscaleOffset(storeOp.getLoc(), byteOffset, 8, rewriter),
-          adaptor.getSource());
+          storeOp, adaptor.getTarget(), elementOffset, adaptor.getSource());
     } else if (oldType.isF32()) {
       rewriter.replaceOpWithNewOp<IREE::VM::BufferStoreF32Op>(
-          storeOp, adaptor.getTarget(),
-          unscaleOffset(storeOp.getLoc(), byteOffset, 4, rewriter),
-          adaptor.getSource());
+          storeOp, adaptor.getTarget(), elementOffset, adaptor.getSource());
     } else if (oldType.isF64()) {
       rewriter.replaceOpWithNewOp<IREE::VM::BufferStoreF64Op>(
-          storeOp, adaptor.getTarget(),
-          unscaleOffset(storeOp.getLoc(), byteOffset, 8, rewriter),
-          adaptor.getSource());
+          storeOp, adaptor.getTarget(), elementOffset, adaptor.getSource());
     } else {
       return rewriter.notifyMatchFailure(storeOp,
                                          "invalid buffer element type");
diff --git a/compiler/src/iree/compiler/Dialect/VM/Conversion/UtilToVM/test/buffer_ops.mlir b/compiler/src/iree/compiler/Dialect/VM/Conversion/UtilToVM/test/buffer_ops.mlir
index b6eb2ef..901ef16 100644
--- a/compiler/src/iree/compiler/Dialect/VM/Conversion/UtilToVM/test/buffer_ops.mlir
+++ b/compiler/src/iree/compiler/Dialect/VM/Conversion/UtilToVM/test/buffer_ops.mlir
@@ -139,10 +139,10 @@
 func.func @buffer_fill_i32(%arg0: !util.buffer, %arg1: index, %arg2: i32) {
   %c100 = arith.constant 100 : index
   %c200 = arith.constant 200 : index
-  // CHECK-32-DAG: %[[C100:.+]] = vm.const.i64 100
-  // CHECK-32-DAG: %[[C200:.+]] = vm.const.i64 200
-  // CHECK-32: vm.buffer.fill.i32 %arg0, %[[C100]], %[[C200]], %arg2 : i32 -> !vm.buffer
-  // CHECK-64: vm.buffer.fill.i32 %arg0, %c100, %c200, %arg2 : i32 -> !vm.buffer
+  // CHECK-32-DAG: %[[C25:.+]] = vm.const.i64 25
+  // CHECK-32-DAG: %[[C50:.+]] = vm.const.i64 50
+  // CHECK-32: vm.buffer.fill.i32 %arg0, %[[C25]], %[[C50]], %arg2 : i32 -> !vm.buffer
+  // CHECK-64: vm.buffer.fill.i32 %arg0, %c25, %c50, %arg2 : i32 -> !vm.buffer
   util.buffer.fill %arg2, %arg0[%c100 for %c200] : i32 -> !util.buffer{%arg1}
   return
 }
@@ -151,13 +151,13 @@
 
 // CHECK-LABEL: @buffer_fill_i64
 func.func @buffer_fill_i64(%arg0: !util.buffer, %arg1: index, %arg2: i64) {
-  %c100 = arith.constant 100 : index
-  %c200 = arith.constant 200 : index
-  // CHECK-32-DAG: %[[C100:.+]] = vm.const.i64 100
-  // CHECK-32-DAG: %[[C200:.+]] = vm.const.i64 200
-  // CHECK-32: vm.buffer.fill.i64 %arg0, %[[C100]], %[[C200]], %arg2 : i64 -> !vm.buffer
-  // CHECK-64: vm.buffer.fill.i64 %arg0, %c100, %c200, %arg2 : i64 -> !vm.buffer
-  util.buffer.fill %arg2, %arg0[%c100 for %c200] : i64 -> !util.buffer{%arg1}
+  %c104 = arith.constant 104 : index
+  %c208 = arith.constant 208 : index
+  // CHECK-32-DAG: %[[C13:.+]] = vm.const.i64 13
+  // CHECK-32-DAG: %[[C26:.+]] = vm.const.i64 26
+  // CHECK-32: vm.buffer.fill.i64 %arg0, %[[C13]], %[[C26]], %arg2 : i64 -> !vm.buffer
+  // CHECK-64: vm.buffer.fill.i64 %arg0, %c13, %c26, %arg2 : i64 -> !vm.buffer
+  util.buffer.fill %arg2, %arg0[%c104 for %c208] : i64 -> !util.buffer{%arg1}
   return
 }
 
diff --git a/compiler/src/iree/compiler/Dialect/VM/IR/VMOpcodesCore.td b/compiler/src/iree/compiler/Dialect/VM/IR/VMOpcodesCore.td
index b170040..b271b5c 100644
--- a/compiler/src/iree/compiler/Dialect/VM/IR/VMOpcodesCore.td
+++ b/compiler/src/iree/compiler/Dialect/VM/IR/VMOpcodesCore.td
@@ -22,7 +22,7 @@
 // but we are a long way out to stabilizing this format :)
 //
 // Some opcodes require an extension prefix to indicate that runtime support
-// is optional. An op with the ExtI64 trait will require VM_OPC_ExtI64, for
+// is optional. An op with the ExtF64 trait will require VM_OPC_ExtF64, for
 // example. Ops that bridge extension sets have a canonical form that may
 // require multiple prefix codes (for example, the i64<->f64 extensions).
 
@@ -34,7 +34,7 @@
                       VM_OPC prefix = ?,
                       list<VM_OPC> cases> :
     IntEnumAttr<I8, name, description, cases> {
-  let cppNamespace = "IREE::VM";
+  let cppNamespace = "::mlir::iree_compiler::IREE::VM";
   let returnType = cppNamespace # "::" # name;
   let underlyingType = "uint8_t";
   let convertFromStorage = "static_cast<" # returnType # ">($_self.getInt())";
@@ -47,6 +47,8 @@
   string opcodeEnumTag = enumTag;
 }
 
+// Next available opcode: 0x7A
+
 // Globals:
 def VM_OPC_GlobalLoadI32         : VM_OPC<0x00, "GlobalLoadI32">;
 def VM_OPC_GlobalStoreI32        : VM_OPC<0x01, "GlobalStoreI32">;
@@ -170,6 +172,7 @@
 def VM_OPC_CmpNZRef              : VM_OPC<0x55, "CmpNZRef">;
 
 // Control flow:
+def VM_OPC_Block                 : VM_OPC<0x79, "Block">;
 def VM_OPC_Branch                : VM_OPC<0x56, "Branch">;
 def VM_OPC_CondBranch            : VM_OPC<0x57, "CondBranch">;
 def VM_OPC_Call                  : VM_OPC<0x58, "Call">;
@@ -358,6 +361,8 @@
     VM_OPC_BufferCopy,
     VM_OPC_BufferCompare,
 
+    VM_OPC_Block,
+
     // Extension opcodes (0xE0-0xFF):
     VM_OPC_PrefixExtF32,  // VM_ExtF32OpcodeAttr
     VM_OPC_PrefixExtF64,  // VM_ExtF64OpcodeAttr
diff --git a/compiler/src/iree/compiler/Dialect/VM/IR/VMOps.td b/compiler/src/iree/compiler/Dialect/VM/IR/VMOps.td
index 8276dc5..5f916c0 100644
--- a/compiler/src/iree/compiler/Dialect/VM/IR/VMOps.td
+++ b/compiler/src/iree/compiler/Dialect/VM/IR/VMOps.td
@@ -1306,7 +1306,7 @@
       MemoryEffects<[MemWrite]>,
     ])> {
   let description = [{
-    Fills a range of the buffer with the given value, like memset.
+    Fills an element range of the buffer with the given value, like memset.
   }];
 
   let arguments = (ins
@@ -1364,9 +1364,7 @@
       MemoryEffects<[MemRead]>,
     ])> {
   let description = [{
-    Loads a value from the buffer at the given offset. Offsets must match the
-    natural alignment of the loaded type and will be coerced to the alignment
-    at runtime automatically.
+    Loads a value from the buffer at the given element offset.
   }];
 
   let arguments = (ins
@@ -1397,9 +1395,7 @@
       MemoryEffects<[MemWrite]>,
     ])> {
   let description = [{
-    Stores a value to the buffer at the given offset. Offsets must match the
-    natural alignment of the loaded type and will be coerced to the alignment
-    at runtime automatically.
+    Stores a value to the buffer at the given element offset.
   }];
 
   let arguments = (ins
diff --git a/compiler/src/iree/compiler/Dialect/VM/IR/VMTraits.h b/compiler/src/iree/compiler/Dialect/VM/IR/VMTraits.h
index 70faec2..7a0c981 100644
--- a/compiler/src/iree/compiler/Dialect/VM/IR/VMTraits.h
+++ b/compiler/src/iree/compiler/Dialect/VM/IR/VMTraits.h
@@ -42,15 +42,6 @@
 };
 
 template <typename ConcreteType>
-class ExtI64 : public OpTrait::TraitBase<ConcreteType, ExtI64> {
- public:
-  static LogicalResult verifyTrait(Operation *op) {
-    // TODO(benvanik): verify i64 ext is supported.
-    return success();
-  }
-};
-
-template <typename ConcreteType>
 class ExtF32 : public OpTrait::TraitBase<ConcreteType, ExtF32> {
  public:
   static LogicalResult verifyTrait(Operation *op) {
diff --git a/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BUILD b/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BUILD
index 43b609a..9cf32a1 100644
--- a/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BUILD
+++ b/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BUILD
@@ -31,6 +31,7 @@
         "//compiler/src/iree/compiler/Dialect/Util/IR",
         "//compiler/src/iree/compiler/Dialect/Util/Transforms",
         "//compiler/src/iree/compiler/Dialect/VM/Analysis",
+        "//compiler/src/iree/compiler/Dialect/VM/Conversion",
         "//compiler/src/iree/compiler/Dialect/VM/IR",
         "//compiler/src/iree/compiler/Dialect/VM/Transforms",
         "//compiler/src/iree/compiler/Dialect/VM/Utils:CallingConvention",
diff --git a/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BytecodeEncoder.cpp b/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BytecodeEncoder.cpp
index 85e1d4e..9dedbdb 100644
--- a/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BytecodeEncoder.cpp
+++ b/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BytecodeEncoder.cpp
@@ -9,6 +9,7 @@
 #include "iree/compiler/Dialect/Util/IR/UtilTypes.h"
 #include "iree/compiler/Dialect/VM/Analysis/RegisterAllocation.h"
 #include "iree/compiler/Dialect/VM/IR/VMDialect.h"
+#include "iree/compiler/Dialect/VM/IR/VMTypes.h"
 #include "llvm/ADT/STLExtras.h"
 #include "mlir/IR/Attributes.h"
 #include "mlir/IR/Diagnostics.h"
@@ -32,7 +33,7 @@
 
   LogicalResult beginBlock(Block *block) override {
     blockOffsets_[block] = bytecode_.size();
-    return success();
+    return writeUint8(static_cast<uint8_t>(Opcode::Block));
   }
 
   LogicalResult endBlock(Block *block) override { return success(); }
@@ -389,6 +390,7 @@
 
   debugDatabase.addFunctionSourceMap(funcOp, sourceMap);
 
+  size_t finalLength = encoder.getOffset();
   if (failed(encoder.ensureAlignment(8))) {
     funcOp.emitError() << "failed to pad function";
     return std::nullopt;
@@ -399,6 +401,8 @@
     return std::nullopt;
   }
   result.bytecodeData = bytecodeData.value();
+  result.bytecodeLength = finalLength;
+  result.blockCount = funcOp.getBlocks().size();
   result.i32RegisterCount = registerAllocation.getMaxI32RegisterOrdinal() + 1;
   result.refRegisterCount = registerAllocation.getMaxRefRegisterOrdinal() + 1;
   return result;
diff --git a/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BytecodeEncoder.h b/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BytecodeEncoder.h
index f1e7eeb..8463980 100644
--- a/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BytecodeEncoder.h
+++ b/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BytecodeEncoder.h
@@ -18,8 +18,13 @@
 namespace VM {
 
 struct EncodedBytecodeFunction {
-  // Encoded bytecode data for the function body.
+  // Encoded bytecode data for the function body including padding.
   std::vector<uint8_t> bytecodeData;
+  // Precise size of the bytecode.
+  size_t bytecodeLength = 0;
+
+  // Total number of blocks including the entry block.
+  uint16_t blockCount = 0;
 
   // Total i32 register slots required for execution.
   // Note that larger types also use these slots (i64=2xi32).
@@ -32,7 +37,7 @@
 class BytecodeEncoder : public VMFuncEncoder {
  public:
   // Matches IREE_VM_BYTECODE_VERSION_MAJOR.
-  static constexpr uint32_t kVersionMajor = 13;
+  static constexpr uint32_t kVersionMajor = 14;
   // Matches IREE_VM_BYTECODE_VERSION_MINOR.
   static constexpr uint32_t kVersionMinor = 0;
   static constexpr uint32_t kVersion = (kVersionMajor << 16) | kVersionMinor;
diff --git a/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BytecodeModuleTarget.cpp b/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BytecodeModuleTarget.cpp
index 31cbebe..2ab87eb 100644
--- a/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BytecodeModuleTarget.cpp
+++ b/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BytecodeModuleTarget.cpp
@@ -179,8 +179,9 @@
 // Canonicalizes the module to its final form prior to emission.
 // This verifies that we only have ops we can serialize and performs any of the
 // required transformations (such as debug op stripping).
-static LogicalResult canonicalizeModule(BytecodeTargetOptions targetOptions,
-                                        IREE::VM::ModuleOp moduleOp) {
+static LogicalResult canonicalizeModule(
+    IREE::VM::BytecodeTargetOptions bytecodeOptions,
+    IREE::VM::ModuleOp moduleOp) {
   RewritePatternSet patterns(moduleOp.getContext());
   ConversionTarget target(*moduleOp.getContext());
   target.addLegalDialect<IREE::VM::VMDialect>();
@@ -198,7 +199,7 @@
     // Debug ops must not be present when stripping.
     // TODO(benvanik): add RemoveDisabledDebugOp pattern.
     if (op.hasTrait<OpTrait::IREE::VM::DebugOnly>() &&
-        targetOptions.stripDebugOps) {
+        bytecodeOptions.stripDebugOps) {
       target.setOpAction(op, ConversionTarget::LegalizationAction::Illegal);
     }
   }
@@ -219,7 +220,7 @@
   modulePasses.addPass(IREE::VM::createGlobalInitializationPass());
   modulePasses.addPass(IREE::VM::createDropEmptyModuleInitializersPass());
 
-  if (targetOptions.optimize) {
+  if (bytecodeOptions.optimize) {
     // TODO(benvanik): run this as part of a fixed-point iteration.
     modulePasses.addPass(mlir::createInlinerPass());
     modulePasses.addPass(mlir::createCSEPass());
@@ -274,9 +275,9 @@
 }
 
 // Returns a serialized function signature.
-static iree_vm_FunctionSignatureDef_ref_t makeExportFunctionSignatureDef(
-    IREE::VM::ExportOp exportOp, IREE::VM::FuncOp funcOp,
-    llvm::DenseMap<Type, int> &typeTable, FlatbufferBuilder &fbb) {
+static iree_vm_FunctionSignatureDef_ref_t makeFunctionSignatureDef(
+    IREE::VM::FuncOp funcOp, llvm::DenseMap<Type, int> &typeTable,
+    FlatbufferBuilder &fbb) {
   // Generate the signature calling convention string based on types.
   auto cconv = makeCallingConventionString(funcOp);
   if (!cconv.has_value()) return {};
@@ -314,6 +315,20 @@
                                     cconv.value(), /*attrsRef=*/0, fbb);
 }
 
+// Walks |rootOp| to find all VM features required by it and its children.
+static iree_vm_FeatureBits_enum_t findRequiredFeatures(Operation *rootOp) {
+  iree_vm_FeatureBits_enum_t result = 0;
+  rootOp->walk([&](Operation *op) {
+    if (op->hasTrait<OpTrait::IREE::VM::ExtF32>()) {
+      result |= iree_vm_FeatureBits_EXT_F32;
+    }
+    if (op->hasTrait<OpTrait::IREE::VM::ExtF64>()) {
+      result |= iree_vm_FeatureBits_EXT_F64;
+    }
+  });
+  return result;
+}
+
 // Builds a complete BytecodeModuleDef FlatBuffer object in |fbb|.
 // The order of the encoding is ordered to ensure that all metadata is at the
 // front of the resulting buffer. Large read-only data and bytecode blobs always
@@ -325,8 +340,10 @@
 // here during serialization but a much more trivial (and cache-friendly)
 // representation at runtime.
 static LogicalResult buildFlatBufferModule(
-    BytecodeTargetOptions targetOptions, IREE::VM::ModuleOp moduleOp,
-    MutableArrayRef<RodataRef> rodataRefs, FlatbufferBuilder &fbb) {
+    IREE::VM::TargetOptions vmOptions,
+    IREE::VM::BytecodeTargetOptions bytecodeOptions,
+    IREE::VM::ModuleOp moduleOp, MutableArrayRef<RodataRef> rodataRefs,
+    FlatbufferBuilder &fbb) {
   // Start the buffer so that we can begin recording data prior to the root
   // table (which we do at the very end). This does not change the layout of the
   // file and is only used to prime the flatcc builder.
@@ -376,20 +393,23 @@
   SmallVector<iree_vm_FunctionDescriptor_t, 8> functionDescriptors;
   bytecodeDataParts.resize(internalFuncOps.size());
   functionDescriptors.resize(internalFuncOps.size());
+  iree_vm_FeatureBits_enum_t moduleRequirements = 0;
   size_t totalBytecodeLength = 0;
-  for (auto funcOp : llvm::enumerate(internalFuncOps)) {
+  for (auto [i, funcOp] : llvm::enumerate(internalFuncOps)) {
     auto encodedFunction = BytecodeEncoder::encodeFunction(
-        funcOp.value(), typeOrdinalMap, symbolTable, debugDatabase);
+        funcOp, typeOrdinalMap, symbolTable, debugDatabase);
     if (!encodedFunction) {
-      return funcOp.value().emitError() << "failed to encode function bytecode";
+      return funcOp.emitError() << "failed to encode function bytecode";
     }
+    auto funcRequirements = findRequiredFeatures(funcOp);
+    moduleRequirements |= funcRequirements;
     iree_vm_FunctionDescriptor_assign(
-        &functionDescriptors[funcOp.index()], totalBytecodeLength,
-        encodedFunction->bytecodeData.size(), encodedFunction->i32RegisterCount,
-        encodedFunction->refRegisterCount);
+        &functionDescriptors[i], totalBytecodeLength,
+        encodedFunction->bytecodeLength, funcRequirements,
+        /*reserved=*/0u, encodedFunction->blockCount,
+        encodedFunction->i32RegisterCount, encodedFunction->refRegisterCount);
     totalBytecodeLength += encodedFunction->bytecodeData.size();
-    bytecodeDataParts[funcOp.index()] =
-        std::move(encodedFunction->bytecodeData);
+    bytecodeDataParts[i] = std::move(encodedFunction->bytecodeData);
   }
   flatbuffers_uint8_vec_start(fbb);
   uint8_t *bytecodeDataPtr =
@@ -399,8 +419,7 @@
   // for both security and determinism).
   memset(bytecodeDataPtr, 0, totalBytecodeLength);
   size_t currentBytecodeOffset = 0;
-  for (const auto &it : llvm::enumerate(internalFuncOps)) {
-    int ordinal = it.index();
+  for (const auto &[ordinal, _] : llvm::enumerate(internalFuncOps)) {
     auto data = std::move(bytecodeDataParts[ordinal]);
     std::memcpy(bytecodeDataPtr + currentBytecodeOffset, data.data(),
                 data.size());
@@ -450,16 +469,18 @@
   // NOTE: rwdata is currently unused.
   SmallVector<iree_vm_RwdataSegmentDef_ref_t, 8> rwdataSegmentRefs;
 
+  auto signatureRefs =
+      llvm::to_vector<8>(llvm::map_range(internalFuncOps, [&](auto funcOp) {
+        return makeFunctionSignatureDef(funcOp, typeOrdinalMap, fbb);
+      }));
+
   auto exportFuncRefs =
       llvm::to_vector<8>(llvm::map_range(exportFuncOps, [&](auto exportOp) {
         auto localNameRef = fbb.createString(exportOp.getExportName());
         auto funcOp =
             symbolTable.lookup<IREE::VM::FuncOp>(exportOp.getFunctionRef());
-        auto signatureRef = makeExportFunctionSignatureDef(exportOp, funcOp,
-                                                           typeOrdinalMap, fbb);
         iree_vm_ExportFunctionDef_start(fbb);
         iree_vm_ExportFunctionDef_local_name_add(fbb, localNameRef);
-        iree_vm_ExportFunctionDef_signature_add(fbb, signatureRef);
         iree_vm_ExportFunctionDef_internal_ordinal_add(
             fbb, funcOp.getOrdinal()->getLimitedValue());
         return iree_vm_ExportFunctionDef_end(fbb);
@@ -516,7 +537,8 @@
   // of the file.
   auto rodataSegmentsRef = fbb.createOffsetVecDestructive(rodataSegmentRefs);
   auto rwdataSegmentsRef = fbb.createOffsetVecDestructive(rwdataSegmentRefs);
-  auto exportFuncsOffset = fbb.createOffsetVecDestructive(exportFuncRefs);
+  auto signaturesRef = fbb.createOffsetVecDestructive(signatureRefs);
+  auto exportFuncsRef = fbb.createOffsetVecDestructive(exportFuncRefs);
   auto importFuncsRef = fbb.createOffsetVecDestructive(importFuncRefs);
   auto dependenciesRef = fbb.createOffsetVecDestructive(dependencyRefs);
   auto typesRef = fbb.createOffsetVecDestructive(typeRefs);
@@ -533,21 +555,35 @@
   }
 
   iree_vm_DebugDatabaseDef_ref_t debugDatabaseRef = 0;
-  if (!targetOptions.stripSourceMap) {
+  if (!bytecodeOptions.stripSourceMap) {
     debugDatabaseRef = debugDatabase.build(fbb);
   }
 
   auto moduleNameRef = fbb.createString(
       moduleOp.getSymName().empty() ? "module" : moduleOp.getSymName());
 
+  // TODO(benvanik): let moduleRequirements be a subset of function requirements
+  // so that we can multi-version. For now the moduleRequirements will be the OR
+  // of all functions.
+  iree_vm_FeatureBits_enum_t allowedFeatures = 0;
+  if (vmOptions.f32Extension) allowedFeatures |= iree_vm_FeatureBits_EXT_F32;
+  if (vmOptions.f64Extension) allowedFeatures |= iree_vm_FeatureBits_EXT_F64;
+  if ((moduleRequirements & allowedFeatures) != moduleRequirements) {
+    return moduleOp.emitError()
+           << "module uses features not allowed by flags (requires "
+           << moduleRequirements << ", allowed " << allowedFeatures << ")";
+  }
+
   iree_vm_BytecodeModuleDef_name_add(fbb, moduleNameRef);
   iree_vm_BytecodeModuleDef_version_add(fbb,
                                         moduleOp.getVersion().value_or(0u));
+  iree_vm_BytecodeModuleDef_requirements_add(fbb, moduleRequirements);
   // TODO(benvanik): iree_vm_BytecodeModuleDef_attrs_add
   iree_vm_BytecodeModuleDef_types_add(fbb, typesRef);
   iree_vm_BytecodeModuleDef_dependencies_add(fbb, dependenciesRef);
   iree_vm_BytecodeModuleDef_imported_functions_add(fbb, importFuncsRef);
-  iree_vm_BytecodeModuleDef_exported_functions_add(fbb, exportFuncsOffset);
+  iree_vm_BytecodeModuleDef_exported_functions_add(fbb, exportFuncsRef);
+  iree_vm_BytecodeModuleDef_function_signatures_add(fbb, signaturesRef);
   iree_vm_BytecodeModuleDef_module_state_add(fbb, moduleStateDef);
   iree_vm_BytecodeModuleDef_rodata_segments_add(fbb, rodataSegmentsRef);
   iree_vm_BytecodeModuleDef_rwdata_segments_add(fbb, rwdataSegmentsRef);
@@ -562,28 +598,30 @@
   return success();
 }
 
-LogicalResult translateModuleToBytecode(IREE::VM::ModuleOp moduleOp,
-                                        BytecodeTargetOptions targetOptions,
-                                        llvm::raw_ostream &output) {
+LogicalResult translateModuleToBytecode(
+    IREE::VM::ModuleOp moduleOp, IREE::VM::TargetOptions vmOptions,
+    IREE::VM::BytecodeTargetOptions bytecodeOptions,
+    llvm::raw_ostream &output) {
   moduleOp.getContext()->getOrLoadDialect<IREE::Util::UtilDialect>();
 
-  if (failed(canonicalizeModule(targetOptions, moduleOp))) {
+  if (failed(canonicalizeModule(bytecodeOptions, moduleOp))) {
     return moduleOp.emitError()
            << "failed to canonicalize vm.module to a serializable form";
   }
 
   // Dump VM assembly source listing to a file and annotate IR locations.
-  if (!targetOptions.sourceListing.empty()) {
+  if (!bytecodeOptions.sourceListing.empty()) {
     OpPrintingFlags printFlags;
     printFlags.elideLargeElementsAttrs(8192);
-    if (failed(mlir::generateLocationsFromIR(targetOptions.sourceListing, "vm",
-                                             moduleOp, printFlags))) {
+    if (failed(mlir::generateLocationsFromIR(bytecodeOptions.sourceListing,
+                                             "vm", moduleOp, printFlags))) {
       return moduleOp.emitError() << "failed to write source listing to '"
-                                  << targetOptions.sourceListing << "'";
+                                  << bytecodeOptions.sourceListing << "'";
     }
   }
 
-  if (targetOptions.outputFormat == BytecodeOutputFormat::kAnnotatedMlirText) {
+  if (bytecodeOptions.outputFormat ==
+      BytecodeOutputFormat::kAnnotatedMlirText) {
     // Run register allocation now and put the info in the IR so it's printed.
     for (auto funcOp : moduleOp.getBlock().getOps<IREE::VM::FuncOp>()) {
       if (!funcOp.empty()) {
@@ -597,8 +635,9 @@
   }
 
   // Debug-only formats:
-  if (targetOptions.outputFormat == BytecodeOutputFormat::kMlirText ||
-      targetOptions.outputFormat == BytecodeOutputFormat::kAnnotatedMlirText) {
+  if (bytecodeOptions.outputFormat == BytecodeOutputFormat::kMlirText ||
+      bytecodeOptions.outputFormat ==
+          BytecodeOutputFormat::kAnnotatedMlirText) {
     // Use the standard MLIR text printer.
     moduleOp.getOperation()->print(output);
     output << "\n";
@@ -607,15 +646,15 @@
 
   // Set up the output archive builder based on output format.
   std::unique_ptr<ArchiveWriter> archiveWriter;
-  if (targetOptions.emitPolyglotZip &&
-      targetOptions.outputFormat == BytecodeOutputFormat::kFlatBufferBinary) {
+  if (bytecodeOptions.emitPolyglotZip &&
+      bytecodeOptions.outputFormat == BytecodeOutputFormat::kFlatBufferBinary) {
     archiveWriter =
         std::make_unique<ZIPArchiveWriter>(moduleOp.getLoc(), output);
-  } else if (targetOptions.outputFormat ==
+  } else if (bytecodeOptions.outputFormat ==
              BytecodeOutputFormat::kFlatBufferBinary) {
     archiveWriter =
         std::make_unique<FlatArchiveWriter>(moduleOp.getLoc(), output);
-  } else if (targetOptions.outputFormat ==
+  } else if (bytecodeOptions.outputFormat ==
              BytecodeOutputFormat::kFlatBufferText) {
     archiveWriter =
         std::make_unique<JSONArchiveWriter>(moduleOp.getLoc(), output);
@@ -673,7 +712,8 @@
   // the first few pages need to be accessed to get the metadata and the rest
   // can be large bulk data.
   FlatbufferBuilder fbb;
-  if (failed(buildFlatBufferModule(targetOptions, moduleOp, rodataRefs, fbb))) {
+  if (failed(buildFlatBufferModule(vmOptions, bytecodeOptions, moduleOp,
+                                   rodataRefs, fbb))) {
     return failure();
   }
   if (failed(archiveWriter->flush(fbb))) {
@@ -684,15 +724,17 @@
   return success();
 }
 
-LogicalResult translateModuleToBytecode(mlir::ModuleOp outerModuleOp,
-                                        BytecodeTargetOptions targetOptions,
-                                        llvm::raw_ostream &output) {
+LogicalResult translateModuleToBytecode(
+    mlir::ModuleOp outerModuleOp, IREE::VM::TargetOptions vmOptions,
+    IREE::VM::BytecodeTargetOptions bytecodeOptions,
+    llvm::raw_ostream &output) {
   auto moduleOps = outerModuleOp.getOps<IREE::VM::ModuleOp>();
   if (moduleOps.empty()) {
     return outerModuleOp.emitError()
            << "outer module does not contain a vm.module op";
   }
-  return translateModuleToBytecode(*moduleOps.begin(), targetOptions, output);
+  return translateModuleToBytecode(*moduleOps.begin(), vmOptions,
+                                   bytecodeOptions, output);
 }
 
 void BytecodeTargetOptions::bindOptions(OptionsBinder &binder) {
diff --git a/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BytecodeModuleTarget.h b/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BytecodeModuleTarget.h
index 6d4e884..aeed3c1 100644
--- a/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BytecodeModuleTarget.h
+++ b/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/BytecodeModuleTarget.h
@@ -7,6 +7,7 @@
 #ifndef IREE_COMPILER_DIALECT_VM_TARGET_BYTECODE_BYTECODEMODULETARGET_H_
 #define IREE_COMPILER_DIALECT_VM_TARGET_BYTECODE_BYTECODEMODULETARGET_H_
 
+#include "iree/compiler/Dialect/VM/Conversion/TargetOptions.h"
 #include "iree/compiler/Dialect/VM/IR/VMOps.h"
 #include "iree/compiler/Utils/OptionUtils.h"
 #include "llvm/Support/raw_ostream.h"
@@ -65,12 +66,12 @@
 // serialized module format.
 //
 // Exposed via the --iree-vm-ir-to-bytecode-module translation.
-LogicalResult translateModuleToBytecode(IREE::VM::ModuleOp moduleOp,
-                                        BytecodeTargetOptions targetOptions,
-                                        llvm::raw_ostream &output);
-LogicalResult translateModuleToBytecode(mlir::ModuleOp outerModuleOp,
-                                        BytecodeTargetOptions targetOptions,
-                                        llvm::raw_ostream &output);
+LogicalResult translateModuleToBytecode(
+    IREE::VM::ModuleOp moduleOp, IREE::VM::TargetOptions vmOptions,
+    IREE::VM::BytecodeTargetOptions bytecodeOptions, llvm::raw_ostream &output);
+LogicalResult translateModuleToBytecode(
+    mlir::ModuleOp outerModuleOp, IREE::VM::TargetOptions vmOptions,
+    IREE::VM::BytecodeTargetOptions bytecodeOptions, llvm::raw_ostream &output);
 
 }  // namespace VM
 }  // namespace IREE
diff --git a/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/CMakeLists.txt b/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/CMakeLists.txt
index 6484f0b..a5092db 100644
--- a/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/CMakeLists.txt
+++ b/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/CMakeLists.txt
@@ -34,6 +34,7 @@
     iree::compiler::Dialect::Util::IR
     iree::compiler::Dialect::Util::Transforms
     iree::compiler::Dialect::VM::Analysis
+    iree::compiler::Dialect::VM::Conversion
     iree::compiler::Dialect::VM::IR
     iree::compiler::Dialect::VM::Transforms
     iree::compiler::Dialect::VM::Utils::CallingConvention
diff --git a/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/TranslationRegistration.cpp b/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/TranslationRegistration.cpp
index b654328..35879f8 100644
--- a/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/TranslationRegistration.cpp
+++ b/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/TranslationRegistration.cpp
@@ -20,7 +20,8 @@
       "Translates a vm.module to a bytecode module",
       [](mlir::ModuleOp moduleOp, llvm::raw_ostream &output) {
         return translateModuleToBytecode(
-            moduleOp, BytecodeTargetOptions::FromFlags::get(), output);
+            moduleOp, TargetOptions::FromFlags::get(),
+            BytecodeTargetOptions::FromFlags::get(), output);
       });
 }
 
diff --git a/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/test/module_encoding_smoke.mlir b/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/test/module_encoding_smoke.mlir
index 7cbdc65..3ebcb22 100644
--- a/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/test/module_encoding_smoke.mlir
+++ b/compiler/src/iree/compiler/Dialect/VM/Target/Bytecode/test/module_encoding_smoke.mlir
@@ -4,27 +4,42 @@
 // CHECK: "name": "simple_module"
 vm.module @simple_module {
   // CHECK: "types": [{
-  // CHECK: "full_name": "i32"
+  // CHECK: "full_name": "f32"
 
   // CHECK: "exported_functions":
   // CHECK: "local_name": "func"
   vm.export @func
 
+  // CHECK: "function_signatures":
+  // CHECK-NEXT: "calling_convention": "0f_f"
+
   // CHECK: "function_descriptors":
 
   // CHECK-NEXT: {
   // CHECK-NEXT:   "bytecode_offset": 0
-  // CHECK-NEXT:   "bytecode_length": 8
+  // CHECK-NEXT:   "bytecode_length": 14
+  // CHECK-NEXT:   "requirements": "EXT_F32"
+  // CHECK-NEXT:   "reserved": 0
+  // CHECK-NEXT:   "block_count": 1
   // CHECK-NEXT:   "i32_register_count": 1
   // CHECK-NEXT:   "ref_register_count": 0
   // CHECK-NEXT: }
-  vm.func @func(%arg0 : i32) -> i32 {
-    vm.return %arg0 : i32
+  vm.func @func(%arg0 : f32) -> f32 {
+    %0 = vm.add.f32 %arg0, %arg0 : f32
+    vm.return %0 : f32
   }
 
   //      CHECK: "bytecode_data": [
-  // CHECK-NEXT:   90,
+  // CHECK-NEXT:   121,
+  // CHECK-NEXT:   224,
+  // CHECK-NEXT:   10,
   // CHECK-NEXT:   0,
+  // CHECK-NEXT:   0,
+  // CHECK-NEXT:   0,
+  // CHECK-NEXT:   0,
+  // CHECK-NEXT:   0,
+  // CHECK-NEXT:   0,
+  // CHECK-NEXT:   90,
   // CHECK-NEXT:   1,
   // CHECK-NEXT:   0,
   // CHECK-NEXT:   0,
diff --git a/compiler/src/iree/compiler/Dialect/VM/Transforms/Passes.cpp b/compiler/src/iree/compiler/Dialect/VM/Transforms/Passes.cpp
index 26d6d21..53baa1c 100644
--- a/compiler/src/iree/compiler/Dialect/VM/Transforms/Passes.cpp
+++ b/compiler/src/iree/compiler/Dialect/VM/Transforms/Passes.cpp
@@ -69,7 +69,7 @@
   FunctionLikeNest(passManager)
       .addPass(mlir::createSCFForLoopCanonicalizationPass);
 
-  // This pass is sketchy as it can pessimizes tight loops due to affine
+  // This pass is sketchy as it can pessimize tight loops due to affine
   // treating all indices as signed and the unsigned conversion pass not being
   // able to handle that. The scf.for canonicalization does a decent job of
   // removing trivial loops above and this catches the rest. It inserts nasty
diff --git a/runtime/bindings/tflite/interpreter.c b/runtime/bindings/tflite/interpreter.c
index 4183b34..0aaeb92 100644
--- a/runtime/bindings/tflite/interpreter.c
+++ b/runtime/bindings/tflite/interpreter.c
@@ -589,7 +589,7 @@
   // remain where they currently are for the next invocation.
   for (iree_host_size_t i = 0; i < interpreter->model->output_count; ++i) {
     iree_hal_buffer_t* buffer = (iree_hal_buffer_t*)iree_vm_list_get_ref_deref(
-        interpreter->output_list, i, iree_hal_buffer_get_descriptor());
+        interpreter->output_list, i, &iree_hal_buffer_descriptor);
     TfLiteTensor* tensor = &interpreter->output_tensors[i];
     IREE_RETURN_IF_ERROR(_TfLiteTensorBind(tensor, buffer));
   }
diff --git a/runtime/src/iree/base/alignment.h b/runtime/src/iree/base/alignment.h
index 7be7ebe..a1179f1 100644
--- a/runtime/src/iree/base/alignment.h
+++ b/runtime/src/iree/base/alignment.h
@@ -16,6 +16,7 @@
 #include <stdint.h>
 #include <string.h>
 
+#include "iree/base/attributes.h"
 #include "iree/base/config.h"
 #include "iree/base/target_platform.h"
 
@@ -107,6 +108,37 @@
 #define iree_sizeof_struct(t) iree_host_align(sizeof(t), iree_max_align_t)
 
 //===----------------------------------------------------------------------===//
+// Alignment intrinsics
+//===----------------------------------------------------------------------===//
+
+#if IREE_HAVE_BUILTIN(__builtin_unreachable) || defined(__GNUC__)
+#define IREE_BUILTIN_UNREACHABLE() __builtin_unreachable()
+#elif defined(IREE_COMPILER_MSVC)
+#define IREE_BUILTIN_UNREACHABLE() __assume(false)
+#else
+#define IREE_BUILTIN_UNREACHABLE() ((void)0)
+#endif  // IREE_HAVE_BUILTIN(__builtin_unreachable) || defined(__GNUC__)
+
+#if !defined(__cplusplus)
+#define IREE_DECLTYPE(v) __typeof__(v)
+#else
+#define IREE_DECLTYPE(v) decltype(v)
+#endif  // __cplusplus
+
+#if IREE_HAVE_BUILTIN(__builtin_assume_aligned) || defined(__GNUC__)
+// NOTE: gcc only assumes on the result so we have to reset ptr.
+#define IREE_BUILTIN_ASSUME_ALIGNED(ptr, size) \
+  (ptr = (IREE_DECLTYPE(ptr))(__builtin_assume_aligned((void*)(ptr), (size))))
+#elif 0  // defined(IREE_COMPILER_MSVC)
+#define IREE_BUILTIN_ASSUME_ALIGNED(ptr, size) \
+  (__assume((((uintptr_t)(ptr)) & ((1 << (size))) - 1)) == 0)
+#else
+#define IREE_BUILTIN_ASSUME_ALIGNED(ptr, size) \
+  ((((uintptr_t)(ptr) % (size)) == 0) ? (ptr)  \
+                                      : (IREE_BUILTIN_UNREACHABLE(), (ptr)))
+#endif  // IREE_HAVE_BUILTIN(__builtin_assume_aligned) || defined(__GNUC__)
+
+//===----------------------------------------------------------------------===//
 // Alignment-safe memory accesses
 //===----------------------------------------------------------------------===//
 
diff --git a/runtime/src/iree/base/attributes.h b/runtime/src/iree/base/attributes.h
index bd396a9..de74591 100644
--- a/runtime/src/iree/base/attributes.h
+++ b/runtime/src/iree/base/attributes.h
@@ -17,12 +17,18 @@
 // Any call annotated with this will be relatively stable.
 // Calls without this are considered private to the IREE implementation and
 // should not be relied upon.
-#ifdef __cplusplus
+#if defined(__cplusplus)
 #define IREE_API_EXPORT extern "C"
 #else
 #define IREE_API_EXPORT
 #endif  // __cplusplus
 
+#if defined(__cplusplus)
+#define IREE_API_EXPORT_VARIABLE extern "C"
+#else
+#define IREE_API_EXPORT_VARIABLE extern
+#endif  // __cplusplus
+
 // Denotes a function pointer that is exposed as part of the IREE API.
 // Example:
 //   iree_status_t(IREE_API_PTR* some_callback)(int value);
@@ -33,13 +39,23 @@
 //===----------------------------------------------------------------------===//
 
 // Queries for [[attribute]] identifiers in modern compilers.
-#ifdef __has_attribute
+#if defined(__has_attribute)
 #define IREE_HAVE_ATTRIBUTE(x) __has_attribute(x)
 #else
 #define IREE_HAVE_ATTRIBUTE(x) 0
 #endif  // __has_attribute
 
 //===----------------------------------------------------------------------===//
+// IREE_HAVE_BUILTIN
+//===----------------------------------------------------------------------===//
+
+#if defined(__has_builtin)
+#define IREE_HAVE_BUILTIN(x) __has_builtin(x)
+#else
+#define IREE_HAVE_BUILTIN(x) 0
+#endif  // __has_builtin
+
+//===----------------------------------------------------------------------===//
 // IREE_PRINTF_ATTRIBUTE
 //===----------------------------------------------------------------------===//
 
diff --git a/runtime/src/iree/base/config.h b/runtime/src/iree/base/config.h
index 8490c16..480ac5a 100644
--- a/runtime/src/iree/base/config.h
+++ b/runtime/src/iree/base/config.h
@@ -225,10 +225,10 @@
 // Enables disassembly of vm bytecode functions and stderr dumping of execution.
 // Increases code size quite, lowers VM performance, and is generally unsafe;
 // include only when debugging or running on trusted inputs.
-#ifdef NDEBUG
-#define IREE_VM_EXECUTION_TRACING_ENABLE 0
-#else
+#ifndef NDEBUG
 #define IREE_VM_EXECUTION_TRACING_ENABLE 1
+#else
+#define IREE_VM_EXECUTION_TRACING_ENABLE 0
 #endif  // NDEBUG
 #endif  // !IREE_VM_EXECUTION_TRACING_ENABLE
 
@@ -256,7 +256,14 @@
 // moderate performance improvement (~10-20%) on very heavy VMVX workloads but
 // adds 20-30KB to the binary size.
 #define IREE_VM_BYTECODE_DISPATCH_COMPUTED_GOTO_ENABLE 0
-#endif  // IREE_VM_BYTECODE_DISPATCH_COMPUTED_GOTO_ENABLE
+#endif  // !IREE_VM_BYTECODE_DISPATCH_COMPUTED_GOTO_ENABLE
+
+#if !defined(IREE_VM_BYTECODE_VERIFICATION_ENABLE)
+// Enables verification ensuring input bytecode is well-formed.
+// This increases binary size but should be left on in all cases where untrusted
+// inputs can be provided. Module metadata is always verified.
+#define IREE_VM_BYTECODE_VERIFICATION_ENABLE 1
+#endif  // !IREE_VM_BYTECODE_VERIFICATION_ENABLE
 
 #if !defined(IREE_VM_EXT_F32_ENABLE)
 // Enables the 32-bit floating-point instruction extension.
@@ -273,6 +280,6 @@
 #if !defined(IREE_VM_UBSAN_CHECKABLE_ENABLE)
 // Exposes VMVX kernels to UBSAN checking, else disable UBSAN checking.
 #define IREE_VM_UBSAN_CHECKABLE_ENABLE 0
-#endif  // IREE_VM_UBSAN_CHECKABLE_ENABLE
+#endif  // !IREE_VM_UBSAN_CHECKABLE_ENABLE
 
 #endif  // IREE_BASE_CONFIG_H_
diff --git a/runtime/src/iree/base/internal/debugging.h b/runtime/src/iree/base/internal/debugging.h
index 0bf232c..dec24ac 100644
--- a/runtime/src/iree/base/internal/debugging.h
+++ b/runtime/src/iree/base/internal/debugging.h
@@ -35,7 +35,7 @@
 // We implement this directly in the header with ALWAYS_INLINE so that the
 // stack doesn't get all messed up.
 IREE_ATTRIBUTE_ALWAYS_INLINE static inline void iree_debug_break(void) {
-#if defined(IREE_COMPILER_HAS_BUILTIN_DEBUG_TRAP)
+#if IREE_HAVE_BUILTIN(__builtin_debugtrap)
   __builtin_debugtrap();
 #elif defined(IREE_PLATFORM_WINDOWS)
   __debugbreak();
@@ -50,7 +50,7 @@
 #else
   // NOTE: this is unrecoverable and debugging cannot continue.
   __builtin_trap();
-#endif  // IREE_COMPILER_HAS_BUILTIN_DEBUG_TRAP
+#endif  // __builtin_debugtrap
 }
 
 //===----------------------------------------------------------------------===//
diff --git a/runtime/src/iree/base/target_platform.h b/runtime/src/iree/base/target_platform.h
index 6d4b659..1abd987 100644
--- a/runtime/src/iree/base/target_platform.h
+++ b/runtime/src/iree/base/target_platform.h
@@ -202,16 +202,6 @@
 #endif  // defined(__has_feature)
 
 //==============================================================================
-// IREE_COMPILER_HAS_BUILTIN_DEBUG_TRAP
-//==============================================================================
-
-#if defined __has_builtin
-#if __has_builtin(__builtin_debugtrap)
-#define IREE_COMPILER_HAS_BUILTIN_DEBUG_TRAP 1
-#endif
-#endif
-
-//==============================================================================
 // IREE_PLATFORM_*
 //==============================================================================
 
diff --git a/runtime/src/iree/modules/hal/types.c b/runtime/src/iree/modules/hal/types.c
index 7109b8b..ba24f9e 100644
--- a/runtime/src/iree/modules/hal/types.c
+++ b/runtime/src/iree/modules/hal/types.c
@@ -7,22 +7,28 @@
 #include "iree/modules/hal/types.h"
 
 //===----------------------------------------------------------------------===//
-// Type registration
+// Type wrappers
 //===----------------------------------------------------------------------===//
 
-static iree_vm_ref_type_descriptor_t iree_hal_allocator_descriptor = {0};
-static iree_vm_ref_type_descriptor_t iree_hal_buffer_descriptor = {0};
-static iree_vm_ref_type_descriptor_t iree_hal_buffer_view_descriptor = {0};
-static iree_vm_ref_type_descriptor_t iree_hal_channel_descriptor = {0};
-static iree_vm_ref_type_descriptor_t iree_hal_command_buffer_descriptor = {0};
-static iree_vm_ref_type_descriptor_t iree_hal_descriptor_set_layout_descriptor =
-    {0};
-static iree_vm_ref_type_descriptor_t iree_hal_device_descriptor = {0};
-static iree_vm_ref_type_descriptor_t iree_hal_event_descriptor = {0};
-static iree_vm_ref_type_descriptor_t iree_hal_executable_descriptor = {0};
-static iree_vm_ref_type_descriptor_t iree_hal_fence_descriptor = {0};
-static iree_vm_ref_type_descriptor_t iree_hal_pipeline_layout_descriptor = {0};
-static iree_vm_ref_type_descriptor_t iree_hal_semaphore_descriptor = {0};
+IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_allocator, iree_hal_allocator_t);
+IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_buffer, iree_hal_buffer_t);
+IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_buffer_view, iree_hal_buffer_view_t);
+IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_channel, iree_hal_channel_t);
+IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_command_buffer,
+                             iree_hal_command_buffer_t);
+IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_descriptor_set_layout,
+                             iree_hal_descriptor_set_layout_t);
+IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_device, iree_hal_device_t);
+IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_event, iree_hal_event_t);
+IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_executable, iree_hal_executable_t);
+IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_pipeline_layout,
+                             iree_hal_pipeline_layout_t);
+IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_fence, iree_hal_fence_t);
+IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_semaphore, iree_hal_semaphore_t);
+
+//===----------------------------------------------------------------------===//
+// Type registration
+//===----------------------------------------------------------------------===//
 
 #define IREE_VM_REGISTER_HAL_C_TYPE(type, name, destroy_fn, descriptor)   \
   descriptor.type_name = iree_make_cstring_view(name);                    \
@@ -112,26 +118,6 @@
   return iree_ok_status();
 }
 
-//===----------------------------------------------------------------------===//
-// Type wrappers
-//===----------------------------------------------------------------------===//
-
-IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_allocator, iree_hal_allocator_t);
-IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_buffer, iree_hal_buffer_t);
-IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_buffer_view, iree_hal_buffer_view_t);
-IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_channel, iree_hal_channel_t);
-IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_command_buffer,
-                             iree_hal_command_buffer_t);
-IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_descriptor_set_layout,
-                             iree_hal_descriptor_set_layout_t);
-IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_device, iree_hal_device_t);
-IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_event, iree_hal_event_t);
-IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_executable, iree_hal_executable_t);
-IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_pipeline_layout,
-                             iree_hal_pipeline_layout_t);
-IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_fence, iree_hal_fence_t);
-IREE_VM_DEFINE_TYPE_ADAPTERS(iree_hal_semaphore, iree_hal_semaphore_t);
-
 //===--------------------------------------------------------------------===//
 // Utilities
 //===--------------------------------------------------------------------===//
@@ -139,7 +125,7 @@
 IREE_API_EXPORT iree_hal_buffer_view_t* iree_vm_list_get_buffer_view_assign(
     const iree_vm_list_t* list, iree_host_size_t i) {
   return (iree_hal_buffer_view_t*)iree_vm_list_get_ref_deref(
-      list, i, iree_hal_buffer_view_get_descriptor());
+      list, i, &iree_hal_buffer_view_descriptor);
 }
 
 IREE_API_EXPORT iree_hal_buffer_view_t* iree_vm_list_get_buffer_view_retain(
diff --git a/runtime/src/iree/schemas/bytecode_module_def.fbs b/runtime/src/iree/schemas/bytecode_module_def.fbs
index 259ba91..a281e97 100644
--- a/runtime/src/iree/schemas/bytecode_module_def.fbs
+++ b/runtime/src/iree/schemas/bytecode_module_def.fbs
@@ -10,6 +10,14 @@
 file_identifier "IREE";
 file_extension "vmfb";
 
+// Available runtime features supported by the active VM implementation.
+enum FeatureBits:uint32 (bit_flags) {
+  // 32-bit floating point extension.
+  EXT_F32 = 0,  // 1u << 0
+  // 64-bit floating point extension.
+  EXT_F64 = 1,  // 1u << 1
+}
+
 // Arbitrary key/value reflection attribute.
 table AttrDef {
   key:string;
@@ -82,24 +90,10 @@
   // Local name of the function (excluding the module namespace).
   local_name:string;
 
-  // Signature of the function expected used for verifying that imports match.
-  signature:FunctionSignatureDef;
-
   // Ordinal in the internal_functions table that implements this function.
   internal_ordinal:int32;
 }
 
-// Defines a bytecode function.
-table InternalFunctionDef {
-  // Local name of the function or empty if the names have been stripped.
-  // The full name of the function when referenced from external modules will
-  // include the BytecodeModuleDef.name prefix.
-  local_name:string;
-
-  // Signature of the function used for reflection.
-  signature:FunctionSignatureDef;
-}
-
 table UncompressedDataDef {
 }
 
@@ -146,7 +140,17 @@
   bytecode_offset:int32;
   bytecode_length:int32;
 
-  // TODO(benvanik): remove counts and embed directly in bytecode.
+  // Features required in order to execute this function.
+  // Note that this may be a superset of the module-level features if some
+  // functions are conditionally executed based on runtime-derived capabilities.
+  requirements:FeatureBits;
+
+  // Unused, must be 0.
+  reserved:int16;
+
+  // Total number of blocks in the function.
+  block_count:int16;
+
   // Total number of i32 registers used by the function.
   i32_register_count:int16;
   // Total number of ref registers used by the function.
@@ -239,6 +243,11 @@
   // resolve.
   version:uint32;
 
+  // Features required to load and run bytecode from the module.
+  // Some functions may have additional requirements for when there are multiple
+  // variants selected based on runtime feature detection.
+  requirements:FeatureBits = 0;
+
   // Module-level attributes, if any.
   attrs:[AttrDef];
 
@@ -254,6 +263,9 @@
   // Exported function definitions used to resolve imports.
   exported_functions:[ExportFunctionDef];
 
+  // All local function signatures (internal and exports).
+  function_signatures:[FunctionSignatureDef];
+
   // Read-only data segments (like non-code .text).
   // May optionally be compressed and decompressed by the loader.
   rodata_segments:[RodataSegmentDef];
diff --git a/runtime/src/iree/task/topology.h b/runtime/src/iree/task/topology.h
index 59edd7b..7a1f568 100644
--- a/runtime/src/iree/task/topology.h
+++ b/runtime/src/iree/task/topology.h
@@ -58,7 +58,7 @@
   uint8_t group_index;
 
   // A name assigned to executor workers used for logging/tracing.
-  char name[15];
+  char name[32 - /*group_index*/ 1];
 
   // Processor index in the cpuinfo set.
   uint32_t processor_index;
diff --git a/runtime/src/iree/tooling/trace_replay.h b/runtime/src/iree/tooling/trace_replay.h
index caa157e..db2de14 100644
--- a/runtime/src/iree/tooling/trace_replay.h
+++ b/runtime/src/iree/tooling/trace_replay.h
@@ -89,133 +89,6 @@
                                            yaml_node_t* event_node,
                                            iree_vm_list_t** out_output_list);
 
-// Defines the type of a primitive value.
-typedef enum iree_e2e_test_value_type_e {
-  // Not a value type.
-  IREE_E2E_TEST_VALUE_TYPE_NONE = 0,
-  // int8_t.
-  IREE_E2E_TEST_VALUE_TYPE_I8 = 1,
-  // int16_t.
-  IREE_E2E_TEST_VALUE_TYPE_I16 = 2,
-  // int32_t.
-  IREE_E2E_TEST_VALUE_TYPE_I32 = 3,
-  // int64_t.
-  IREE_E2E_TEST_VALUE_TYPE_I64 = 4,
-  // halft_t.
-  IREE_E2E_TEST_VALUE_TYPE_F16 = 5,
-  // float.
-  IREE_E2E_TEST_VALUE_TYPE_F32 = 6,
-  // double.
-  IREE_E2E_TEST_VALUE_TYPE_F64 = 7,
-} iree_e2e_test_value_type_t;
-
-// Maximum size, in bytes, of any value type we can represent.
-#define IREE_E2E_TEST_VALUE_STORAGE_SIZE 8
-
-// A variant value type.
-typedef struct iree_e2e_test_value_t {
-  iree_e2e_test_value_type_t type;
-  union {
-    int8_t i8;
-    int16_t i16;
-    int32_t i32;
-    int64_t i64;
-    float f32;
-    uint16_t f16_u16;
-    double f64;
-
-    uint8_t value_storage[IREE_E2E_TEST_VALUE_STORAGE_SIZE];  // max size of all
-                                                              // value types
-  };
-} iree_e2e_test_value_t;
-
-static inline iree_e2e_test_value_t iree_e2e_test_value_make_none() {
-  iree_e2e_test_value_t result;
-  result.type = IREE_E2E_TEST_VALUE_TYPE_NONE;
-  return result;
-}
-
-static inline iree_e2e_test_value_t iree_e2e_test_value_make_i8(int8_t value) {
-  iree_e2e_test_value_t result;
-  result.type = IREE_E2E_TEST_VALUE_TYPE_I8;
-  result.i8 = value;
-  return result;
-}
-
-static inline iree_e2e_test_value_t iree_e2e_test_value_make_i16(
-    int16_t value) {
-  iree_e2e_test_value_t result;
-  result.type = IREE_E2E_TEST_VALUE_TYPE_I16;
-  result.i16 = value;
-  return result;
-}
-
-static inline iree_e2e_test_value_t iree_e2e_test_value_make_i32(
-    int32_t value) {
-  iree_e2e_test_value_t result;
-  result.type = IREE_E2E_TEST_VALUE_TYPE_I32;
-  result.i32 = value;
-  return result;
-}
-
-// TODO(#5542): check the value type before accessing the union.
-static inline int32_t iree_e2e_test_value_get_i32(
-    iree_e2e_test_value_t* value) {
-  return value->i32;
-}
-
-static inline iree_e2e_test_value_t iree_e2e_test_value_make_i64(
-    int64_t value) {
-  iree_e2e_test_value_t result;
-  result.type = IREE_E2E_TEST_VALUE_TYPE_I64;
-  result.i64 = value;
-  return result;
-}
-
-// TODO(#5542): check the value type before accessing the union.
-static inline int64_t iree_e2e_test_value_get_i64(
-    iree_e2e_test_value_t* value) {
-  return value->i64;
-}
-
-static inline iree_e2e_test_value_t iree_e2e_test_value_make_f16(
-    uint16_t value) {
-  iree_e2e_test_value_t result;
-  result.type = IREE_E2E_TEST_VALUE_TYPE_F16;
-  result.f16_u16 = value;
-  return result;
-}
-
-static inline iree_e2e_test_value_t iree_e2e_test_value_make_f32(float value) {
-  iree_e2e_test_value_t result;
-  result.type = IREE_E2E_TEST_VALUE_TYPE_F32;
-  result.f32 = value;
-  return result;
-}
-
-// TODO(#5542): check the value type before accessing the union.
-static inline float iree_e2e_test_value_get_f32(iree_e2e_test_value_t* value) {
-  return value->f32;
-}
-
-// TODO(#5542): check the value type before accessing the union.
-static inline uint16_t iree_e2e_test_value_get_f16(
-    iree_e2e_test_value_t* value) {
-  return value->f16_u16;
-}
-
-static inline iree_e2e_test_value_t iree_e2e_test_value_make_f64(double value) {
-  iree_e2e_test_value_t result;
-  result.type = IREE_E2E_TEST_VALUE_TYPE_F64;
-  result.f64 = value;
-  return result;
-}
-
-// TODO(#5542): check the value type before accessing the union.
-static inline double iree_e2e_test_value_get_f64(iree_e2e_test_value_t* value) {
-  return value->f64;
-}
-
 #ifdef __cplusplus
 }  // extern "C"
 #endif  // __cplusplus
diff --git a/runtime/src/iree/vm/buffer.c b/runtime/src/iree/vm/buffer.c
index 8427a07..0cb2974 100644
--- a/runtime/src/iree/vm/buffer.c
+++ b/runtime/src/iree/vm/buffer.c
@@ -12,8 +12,6 @@
 #include "iree/base/tracing.h"
 #include "iree/vm/instance.h"
 
-static iree_vm_ref_type_descriptor_t iree_vm_buffer_descriptor = {0};
-
 IREE_VM_DEFINE_TYPE_ADAPTERS(iree_vm_buffer, iree_vm_buffer_t);
 
 static iree_status_t iree_vm_buffer_map(const iree_vm_buffer_t* buffer,
@@ -141,7 +139,7 @@
 
   // Try to map the source buffer first; no use continuing if we can't read the
   // data to clone.
-  iree_const_byte_span_t source_span;
+  iree_const_byte_span_t source_span = iree_const_byte_span_empty();
   IREE_RETURN_AND_END_ZONE_IF_ERROR(
       z0, iree_vm_buffer_map_ro(source_buffer, source_offset, length, 1,
                                 &source_span));
@@ -192,10 +190,10 @@
     iree_host_size_t length) {
   IREE_ASSERT_ARGUMENT(source_buffer);
   IREE_ASSERT_ARGUMENT(target_buffer);
-  iree_const_byte_span_t source_span;
+  iree_const_byte_span_t source_span = iree_const_byte_span_empty();
   IREE_RETURN_IF_ERROR(iree_vm_buffer_map_ro(source_buffer, source_offset,
                                              length, 1, &source_span));
-  iree_byte_span_t target_span;
+  iree_byte_span_t target_span = iree_byte_span_empty();
   IREE_RETURN_IF_ERROR(iree_vm_buffer_map_rw(target_buffer, target_offset,
                                              length, 1, &target_span));
   memcpy(target_span.data, source_span.data, length);
@@ -208,10 +206,10 @@
     iree_host_size_t length, bool* out_result) {
   IREE_ASSERT_ARGUMENT(lhs_buffer);
   IREE_ASSERT_ARGUMENT(rhs_buffer);
-  iree_const_byte_span_t lhs_span;
+  iree_const_byte_span_t lhs_span = iree_const_byte_span_empty();
   IREE_RETURN_IF_ERROR(
       iree_vm_buffer_map_ro(lhs_buffer, lhs_offset, length, 1, &lhs_span));
-  iree_const_byte_span_t rhs_span;
+  iree_const_byte_span_t rhs_span = iree_const_byte_span_empty();
   IREE_RETURN_IF_ERROR(
       iree_vm_buffer_map_ro(rhs_buffer, rhs_offset, length, 1, &rhs_span));
   *out_result = memcmp(lhs_span.data, rhs_span.data, length) == 0;
@@ -230,10 +228,10 @@
     iree_host_size_t element_count, iree_host_size_t element_length,
     const void* value) {
   IREE_ASSERT_ARGUMENT(target_buffer);
-  iree_byte_span_t span;
-  IREE_RETURN_IF_ERROR(iree_vm_buffer_map_rw(target_buffer, target_offset,
-                                             element_count * element_length,
-                                             element_length, &span));
+  iree_byte_span_t span = iree_byte_span_empty();
+  IREE_RETURN_IF_ERROR(iree_vm_buffer_map_rw(
+      target_buffer, target_offset * element_length,
+      element_count * element_length, element_length, &span));
   switch (element_length) {
     case 1: {
       const uint8_t pattern_value = *(const uint8_t*)value;
@@ -274,10 +272,10 @@
     void* target_ptr, iree_host_size_t element_count,
     iree_host_size_t element_length) {
   IREE_ASSERT_ARGUMENT(source_buffer);
-  iree_const_byte_span_t source_span;
-  IREE_RETURN_IF_ERROR(iree_vm_buffer_map_ro(source_buffer, source_offset,
-                                             element_count * element_length,
-                                             element_length, &source_span));
+  iree_const_byte_span_t source_span = iree_const_byte_span_empty();
+  IREE_RETURN_IF_ERROR(iree_vm_buffer_map_ro(
+      source_buffer, source_offset * element_length,
+      element_count * element_length, element_length, &source_span));
   memcpy(target_ptr, source_span.data, source_span.data_length);
   return iree_ok_status();
 }
@@ -288,10 +286,10 @@
     iree_host_size_t element_length) {
   IREE_ASSERT_ARGUMENT(source_ptr);
   IREE_ASSERT_ARGUMENT(target_buffer);
-  iree_byte_span_t target_span;
-  IREE_RETURN_IF_ERROR(iree_vm_buffer_map_rw(target_buffer, target_offset,
-                                             element_count * element_length,
-                                             element_length, &target_span));
+  iree_byte_span_t target_span = iree_byte_span_empty();
+  IREE_RETURN_IF_ERROR(iree_vm_buffer_map_rw(
+      target_buffer, target_offset * element_length,
+      element_count * element_length, element_length, &target_span));
   memcpy(target_span.data, source_ptr, target_span.data_length);
   return iree_ok_status();
 }
@@ -301,7 +299,6 @@
     // Already registered.
     return iree_ok_status();
   }
-
   iree_vm_buffer_descriptor.destroy = iree_vm_buffer_destroy;
   iree_vm_buffer_descriptor.offsetof_counter =
       offsetof(iree_vm_buffer_t, ref_object.counter);
diff --git a/runtime/src/iree/vm/buffer.h b/runtime/src/iree/vm/buffer.h
index 1d20984..250eb65 100644
--- a/runtime/src/iree/vm/buffer.h
+++ b/runtime/src/iree/vm/buffer.h
@@ -188,6 +188,61 @@
     iree_host_size_t target_offset, iree_host_size_t element_count,
     iree_host_size_t element_length);
 
+// Low-level helper for accessing a typed view of a buffer for read access.
+// The calling function must be safe to return from. Assumes buffer is non-null.
+// Prefer iree_vm_buffer_read_elements for larger reads.
+//
+// Usage (read 4 floats from the buffer):
+//  const float* IREE_RESTRICT buffer_ptr = NULL;
+//  iree_vm_buffer_check_ro(buffer, offset, 4, float, buffer_ptr);
+//  process(buffer_ptr[0], buffer_ptr[1], buffer_ptr[2], buffer_ptr[3]);
+#define iree_vm_buffer_check_ro(buffer, element_offset, element_length,        \
+                                element_type, out_buffer_ptr)                  \
+  {                                                                            \
+    const iree_host_size_t end =                                               \
+        ((element_offset) + (element_length)) * sizeof(element_type);          \
+    if (IREE_UNLIKELY(end > buffer->data.data_length)) {                       \
+      return iree_make_status(IREE_STATUS_OUT_OF_RANGE,                        \
+                              "out-of-bounds access detected (offset=%zu, "    \
+                              "length=%zu, alignment=%zu, buffer length=%zu)", \
+                              (element_offset) * sizeof(element_type),         \
+                              (element_length) * sizeof(element_type),         \
+                              sizeof(element_type), buffer->data.data_length); \
+    }                                                                          \
+    out_buffer_ptr =                                                           \
+        (const element_type*)buffer->data.data + (element_offset);             \
+  }
+
+// Low-level helper for accessing a typed view of a buffer for write access.
+// The calling function must be safe to return from. Assumes buffer is non-null.
+// Prefer iree_vm_buffer_write_elements for larger reads.
+//
+// Usage (write a single float to the buffer):
+//  float* IREE_RESTRICT buffer_ptr = NULL;
+//  iree_vm_buffer_check_rw(buffer, offset, 1, float, buffer_ptr);
+//  buffer_ptr[0] = 1.0f;
+#define iree_vm_buffer_check_rw(buffer, element_offset, element_length,        \
+                                element_type, out_buffer_ptr)                  \
+  {                                                                            \
+    if (IREE_UNLIKELY(!iree_all_bits_set(buffer->access,                       \
+                                         IREE_VM_BUFFER_ACCESS_MUTABLE))) {    \
+      return iree_make_status(                                                 \
+          IREE_STATUS_PERMISSION_DENIED,                                       \
+          "buffer is read-only and cannot be mapped for mutation");            \
+    }                                                                          \
+    const iree_host_size_t end =                                               \
+        ((element_offset) + (element_length)) * sizeof(element_type);          \
+    if (IREE_UNLIKELY(end > buffer->data.data_length)) {                       \
+      return iree_make_status(IREE_STATUS_OUT_OF_RANGE,                        \
+                              "out-of-bounds access detected (offset=%zu, "    \
+                              "length=%zu, alignment=%zu, buffer length=%zu)", \
+                              (element_offset) * sizeof(element_type),         \
+                              (element_length) * sizeof(element_type),         \
+                              sizeof(element_type), buffer->data.data_length); \
+    }                                                                          \
+    out_buffer_ptr = (element_type*)buffer->data.data + (element_offset);      \
+  }
+
 // Returns the a string view referencing the given |value| buffer.
 // The returned view will only be valid for as long as the buffer is live.
 static inline iree_string_view_t iree_vm_buffer_as_string(
diff --git a/runtime/src/iree/vm/bytecode/BUILD b/runtime/src/iree/vm/bytecode/BUILD
index 54257a3..69bd228 100644
--- a/runtime/src/iree/vm/bytecode/BUILD
+++ b/runtime/src/iree/vm/bytecode/BUILD
@@ -22,48 +22,30 @@
 iree_runtime_cc_library(
     name = "module",
     srcs = [
+        "archive.c",
         "disassembler.c",
         "disassembler.h",
         "dispatch.c",
         "dispatch_util.h",
-        "generated/op_table.h",
         "module.c",
         "module_impl.h",
+        "verifier.c",
+        "verifier.h",
     ],
     hdrs = [
+        "archive.h",
         "module.h",
     ],
     deps = [
         "//runtime/src/iree/base",
-        "//runtime/src/iree/base:core_headers",
         "//runtime/src/iree/base:tracing",
         "//runtime/src/iree/base/internal",
-        "//runtime/src/iree/base/internal/flatcc:parsing",
-        "//runtime/src/iree/schemas:bytecode_module_def_c_fbs",
         "//runtime/src/iree/vm",
         "//runtime/src/iree/vm:ops",
+        "//runtime/src/iree/vm/bytecode/utils",
     ],
 )
 
-# TODO(#357): Add a script to update op_table.h.
-# iree_gentbl_cc_library(
-#     name = "op_table_gen",
-#     tbl_outs = [
-#         (["--gen-iree-vm-op-table-defs"], "op_table.h"),
-#     ],
-#     tblgen = "//tools:iree-tblgen",
-#     td_file = "//compiler/src/iree/compiler/Dialect/VM/IR:VMOps.td",
-#     deps = [
-#         "//compiler/src/iree/compiler/Dialect/Util/IR:td_files",
-#         "//compiler/src/iree/compiler/Dialect/VM/IR:td_files",
-#         "@llvm-project//mlir:CallInterfacesTdFiles",
-#         "@llvm-project//mlir:ControlFlowInterfacesTdFiles",
-#         "@llvm-project//mlir:FunctionInterfacesTdFiles",
-#         "@llvm-project//mlir:OpBaseTdFiles",
-#         "@llvm-project//mlir:SideEffectInterfacesTdFiles",
-#     ],
-# )
-
 iree_cmake_extra_content(
     content = """
 if(IREE_BUILD_COMPILER)
diff --git a/runtime/src/iree/vm/bytecode/CMakeLists.txt b/runtime/src/iree/vm/bytecode/CMakeLists.txt
index 7e5a59b..08dc9b7 100644
--- a/runtime/src/iree/vm/bytecode/CMakeLists.txt
+++ b/runtime/src/iree/vm/bytecode/CMakeLists.txt
@@ -14,23 +14,24 @@
   NAME
     module
   HDRS
+    "archive.h"
     "module.h"
   SRCS
+    "archive.c"
     "disassembler.c"
     "disassembler.h"
     "dispatch.c"
     "dispatch_util.h"
-    "generated/op_table.h"
     "module.c"
     "module_impl.h"
+    "verifier.c"
+    "verifier.h"
   DEPS
     iree::base
-    iree::base::core_headers
     iree::base::internal
-    iree::base::internal::flatcc::parsing
     iree::base::tracing
-    iree::schemas::bytecode_module_def_c_fbs
     iree::vm
+    iree::vm::bytecode::utils
     iree::vm::ops
   PUBLIC
 )
diff --git a/runtime/src/iree/vm/bytecode/archive.c b/runtime/src/iree/vm/bytecode/archive.c
new file mode 100644
index 0000000..6ee7cdc
--- /dev/null
+++ b/runtime/src/iree/vm/bytecode/archive.c
@@ -0,0 +1,140 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "iree/vm/bytecode/archive.h"
+
+#include "iree/vm/bytecode/utils/isa.h"
+
+// ZIP local file header (comes immediately before each file in the archive).
+// In order to find the starting offset of the FlatBuffer in a polyglot archive
+// we need to parse this given the variable-length nature of it (we want to
+// be robust to file name and alignment changes).
+//
+// NOTE: all fields are little-endian.
+// NOTE: we don't care about the actual module size here; since we support
+//       streaming archives trying to recover it would require much more
+//       involved processing (we'd need to reference the central directory).
+//       If we wanted to support users repacking ZIPs we'd probably want to
+//       rewrite everything as we store offsets in the FlatBuffer that are
+//       difficult to update after the archive has been produced.
+#define ZIP_LOCAL_FILE_HEADER_SIGNATURE 0x04034B50u
+#if defined(IREE_COMPILER_MSVC)
+#pragma pack(push, 1)
+#endif  // IREE_COMPILER_MSVC
+typedef struct {
+  uint32_t signature;  // ZIP_LOCAL_FILE_HEADER_SIGNATURE
+  uint16_t version;
+  uint16_t general_purpose_flag;
+  uint16_t compression_method;
+  uint16_t last_modified_time;
+  uint16_t last_modified_date;
+  uint32_t crc32;              // 0 for us
+  uint32_t compressed_size;    // 0 for us
+  uint32_t uncompressed_size;  // 0 for us
+  uint16_t file_name_length;
+  uint16_t extra_field_length;
+  // file name (variable size)
+  // extra field (variable size)
+} IREE_ATTRIBUTE_PACKED zip_local_file_header_t;
+#if defined(IREE_COMPILER_MSVC)
+#pragma pack(pop)
+#endif  // IREE_COMPILER_MSVC
+static_assert(sizeof(zip_local_file_header_t) == 30, "bad packing");
+#if !defined(IREE_ENDIANNESS_LITTLE) || !IREE_ENDIANNESS_LITTLE
+#error "little endian required for zip header parsing"
+#endif  // IREE_ENDIANNESS_LITTLE
+
+// Strips any ZIP local file header from |contents| and stores the remaining
+// range in |out_stripped|.
+static iree_status_t iree_vm_bytecode_module_strip_zip_header(
+    iree_const_byte_span_t contents, iree_const_byte_span_t* out_stripped) {
+  // Ensure there's at least some bytes we can check for the header.
+  // Since we're only looking to strip zip stuff here we can check on that.
+  if (!contents.data ||
+      contents.data_length < sizeof(zip_local_file_header_t)) {
+    memmove(out_stripped, &contents, sizeof(contents));
+    return iree_ok_status();
+  }
+
+  // Check to see if there's a zip local header signature.
+  // For a compliant zip file this is expected to start at offset 0.
+  const zip_local_file_header_t* header =
+      (const zip_local_file_header_t*)contents.data;
+  if (header->signature != ZIP_LOCAL_FILE_HEADER_SIGNATURE) {
+    // No signature found, probably not a ZIP.
+    memmove(out_stripped, &contents, sizeof(contents));
+    return iree_ok_status();
+  }
+
+  // Compute the starting offset of the file.
+  // Note that we still don't know (or care) if it's the file we want; actual
+  // FlatBuffer verification happens later on.
+  uint32_t offset =
+      sizeof(*header) + header->file_name_length + header->extra_field_length;
+  if (offset > contents.data_length) {
+    // Is a ZIP but doesn't have enough data; error out with something more
+    // useful than the FlatBuffer verification failing later on given that here
+    // we know this isn't a FlatBuffer.
+    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                            "archive self-reports as a zip but does not have "
+                            "enough data to contain a module");
+  }
+
+  *out_stripped = iree_make_const_byte_span(contents.data + offset,
+                                            contents.data_length - offset);
+  return iree_ok_status();
+}
+
+IREE_API_EXPORT iree_status_t iree_vm_bytecode_archive_parse_header(
+    iree_const_byte_span_t archive_contents,
+    iree_const_byte_span_t* out_flatbuffer_contents,
+    iree_host_size_t* out_rodata_offset) {
+  // Slice off any polyglot zip header we have prior to the base of the module.
+  iree_const_byte_span_t module_contents = iree_const_byte_span_empty();
+  IREE_RETURN_IF_ERROR(iree_vm_bytecode_module_strip_zip_header(
+      archive_contents, &module_contents));
+
+  // Verify there's enough data to safely check the FlatBuffer header.
+  if (!module_contents.data || module_contents.data_length < 16) {
+    return iree_make_status(
+        IREE_STATUS_INVALID_ARGUMENT,
+        "FlatBuffer data is not present or less than 16 bytes (%zu total)",
+        module_contents.data_length);
+  }
+
+  // Read the size prefix from the head of the module contents; this should be
+  // a 4 byte value indicating the total size of the FlatBuffer data.
+  size_t length_prefix = 0;
+  flatbuffers_read_size_prefix((void*)module_contents.data, &length_prefix);
+
+  // Verify the length prefix is within bounds (always <= the remaining module
+  // bytes).
+  size_t length_remaining =
+      module_contents.data_length - sizeof(flatbuffers_uoffset_t);
+  if (length_prefix > length_remaining) {
+    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                            "FlatBuffer length prefix out of bounds (prefix is "
+                            "%zu but only %zu available)",
+                            length_prefix, length_remaining);
+  }
+
+  // Form the range of bytes containing just the FlatBuffer data.
+  iree_const_byte_span_t flatbuffer_contents = iree_make_const_byte_span(
+      module_contents.data + sizeof(flatbuffers_uoffset_t), length_prefix);
+
+  if (out_flatbuffer_contents) {
+    *out_flatbuffer_contents = flatbuffer_contents;
+  }
+  if (out_rodata_offset) {
+    // rodata begins immediately following the FlatBuffer in memory.
+    iree_host_size_t rodata_offset = iree_host_align(
+        (iree_host_size_t)(flatbuffer_contents.data - archive_contents.data) +
+            length_prefix,
+        IREE_VM_ARCHIVE_SEGMENT_ALIGNMENT);
+    *out_rodata_offset = rodata_offset;
+  }
+  return iree_ok_status();
+}
diff --git a/runtime/src/iree/vm/bytecode/archive.h b/runtime/src/iree/vm/bytecode/archive.h
new file mode 100644
index 0000000..ec4b395
--- /dev/null
+++ b/runtime/src/iree/vm/bytecode/archive.h
@@ -0,0 +1,35 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef IREE_VM_BYTECODE_ARCHIVE_H_
+#define IREE_VM_BYTECODE_ARCHIVE_H_
+
+#include "iree/base/api.h"
+#include "iree/vm/api.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif  // __cplusplus
+
+// Alignment applied to each segment of the archive.
+// All embedded file contents (FlatBuffers, rodata, etc) are aligned to this
+// boundary.
+#define IREE_VM_ARCHIVE_SEGMENT_ALIGNMENT 64
+
+// Parses the module archive header in |archive_contents|.
+// The subrange containing the FlatBuffer data is returned as well as the
+// offset where external rodata begins. Note that archives may have
+// non-contiguous layouts!
+IREE_API_EXPORT iree_status_t iree_vm_bytecode_archive_parse_header(
+    iree_const_byte_span_t archive_contents,
+    iree_const_byte_span_t* out_flatbuffer_contents,
+    iree_host_size_t* out_rodata_offset);
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif  // __cplusplus
+
+#endif  // IREE_VM_BYTECODE_ARCHIVE_H_
diff --git a/runtime/src/iree/vm/bytecode/disassembler.c b/runtime/src/iree/vm/bytecode/disassembler.c
index 6b0f5bf..e0fbab0 100644
--- a/runtime/src/iree/vm/bytecode/disassembler.c
+++ b/runtime/src/iree/vm/bytecode/disassembler.c
@@ -8,7 +8,6 @@
 
 #include <inttypes.h>
 
-#include "iree/base/config.h"
 #include "iree/vm/ops.h"
 
 #define BEGIN_DISASM_PREFIX(op_name, ext) \
@@ -64,39 +63,39 @@
 #define VM_ParseBranchOperands(operands_name) \
   VM_DecBranchOperandsImpl(bytecode_data, &pc)
 #define VM_ParseOperandRegI32(name) \
-  OP_I16(0) & regs->i32_mask;       \
-  pc += kRegSize;
-#define VM_ParseOperandRegI64(name)  \
-  OP_I16(0) & (regs->i32_mask & ~1); \
-  pc += kRegSize;
+  OP_I16(0);                        \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_ParseOperandRegI64(name) \
+  OP_I16(0);                        \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
 #define VM_ParseOperandRegF32(name) \
-  OP_I16(0) & regs->i32_mask;       \
-  pc += kRegSize;
-#define VM_ParseOperandRegF64(name)  \
-  OP_I16(0) & (regs->i32_mask & ~1); \
-  pc += kRegSize;
+  OP_I16(0);                        \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_ParseOperandRegF64(name) \
+  OP_I16(0);                        \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
 #define VM_ParseOperandRegRef(name, out_is_move)                    \
-  OP_I16(0) & regs->ref_mask;                                       \
+  OP_I16(0) & IREE_REF_REGISTER_MASK;                               \
   *(out_is_move) = 0; /*= OP_I16(0) & IREE_REF_REGISTER_MOVE_BIT;*/ \
-  pc += kRegSize;
+  pc += IREE_REGISTER_ORDINAL_SIZE;
 #define VM_ParseVariadicOperands(name) \
   VM_DecVariadicOperandsImpl(bytecode_data, &pc)
 #define VM_ParseResultRegI32(name) \
-  OP_I16(0) & regs->i32_mask;      \
-  pc += kRegSize;
-#define VM_ParseResultRegI64(name)   \
-  OP_I16(0) & (regs->i32_mask & ~1); \
-  pc += kRegSize;
+  OP_I16(0);                       \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_ParseResultRegI64(name) \
+  OP_I16(0);                       \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
 #define VM_ParseResultRegF32(name) \
-  OP_I16(0) & regs->i32_mask;      \
-  pc += kRegSize;
-#define VM_ParseResultRegF64(name)   \
-  OP_I16(0) & (regs->i32_mask & ~1); \
-  pc += kRegSize;
+  OP_I16(0);                       \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_ParseResultRegF64(name) \
+  OP_I16(0);                       \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
 #define VM_ParseResultRegRef(name, out_is_move)                     \
-  OP_I16(0) & regs->ref_mask;                                       \
+  OP_I16(0) & IREE_REF_REGISTER_MASK;                               \
   *(out_is_move) = 0; /*= OP_I16(0) & IREE_REF_REGISTER_MOVE_BIT;*/ \
-  pc += kRegSize;
+  pc += IREE_REGISTER_ORDINAL_SIZE;
 #define VM_ParseVariadicResults(name) VM_ParseVariadicOperands(name)
 
 #define EMIT_REG_NAME(reg)                \
@@ -1320,6 +1319,7 @@
     DISASM_OP_CORE_BINARY_I32(RemI32S, "vm.rem.i32.s");
     DISASM_OP_CORE_BINARY_I32(RemI32U, "vm.rem.i32.u");
     DISASM_OP_CORE_TERNARY_I32(FMAI32, "vm.fma.i32");
+    DISASM_OP_CORE_UNARY_I32(AbsI32, "vm.abs.i32");
     DISASM_OP_CORE_UNARY_I32(NotI32, "vm.not.i32");
     DISASM_OP_CORE_BINARY_I32(AndI32, "vm.and.i32");
     DISASM_OP_CORE_BINARY_I32(OrI32, "vm.or.i32");
@@ -1334,6 +1334,7 @@
     DISASM_OP_CORE_BINARY_I64(RemI64S, "vm.rem.i64.s");
     DISASM_OP_CORE_BINARY_I64(RemI64U, "vm.rem.i64.u");
     DISASM_OP_CORE_TERNARY_I64(FMAI64, "vm.fma.i64");
+    DISASM_OP_CORE_UNARY_I64(AbsI64, "vm.abs.i64");
     DISASM_OP_CORE_UNARY_I64(NotI64, "vm.not.i64");
     DISASM_OP_CORE_BINARY_I64(AndI64, "vm.and.i64");
     DISASM_OP_CORE_BINARY_I64(OrI64, "vm.or.i64");
@@ -1515,6 +1516,12 @@
     // Control flow
     //===------------------------------------------------------------------===//
 
+    DISASM_OP(CORE, Block) {
+      IREE_RETURN_IF_ERROR(
+          iree_string_builder_append_string(b, IREE_SV("<block>")));
+      break;
+    }
+
     DISASM_OP(CORE, Branch) {
       int32_t block_pc = VM_ParseBranchTarget("dest");
       const iree_vm_register_remap_list_t* remap_list =
diff --git a/runtime/src/iree/vm/bytecode/disassembler.h b/runtime/src/iree/vm/bytecode/disassembler.h
index 0f3dd49..36c00ce 100644
--- a/runtime/src/iree/vm/bytecode/disassembler.h
+++ b/runtime/src/iree/vm/bytecode/disassembler.h
@@ -9,7 +9,7 @@
 
 #include <stdio.h>
 
-#include "iree/base/string_builder.h"
+#include "iree/base/api.h"
 #include "iree/vm/api.h"
 #include "iree/vm/bytecode/dispatch_util.h"
 #include "iree/vm/bytecode/module_impl.h"
diff --git a/runtime/src/iree/vm/bytecode/dispatch.c b/runtime/src/iree/vm/bytecode/dispatch.c
index 5e2dbea..220f9c5 100644
--- a/runtime/src/iree/vm/bytecode/dispatch.c
+++ b/runtime/src/iree/vm/bytecode/dispatch.c
@@ -28,7 +28,7 @@
 // no swapping hazards (such as 0->1,1->0). The register allocator in the
 // compiler should ensure this is the case when it can occur.
 static void iree_vm_bytecode_dispatch_remap_branch_registers(
-    const iree_vm_registers_t regs,
+    int32_t* IREE_RESTRICT regs_i32, iree_vm_ref_t* IREE_RESTRICT regs_ref,
     const iree_vm_register_remap_list_t* IREE_RESTRICT remap_list) {
   for (int i = 0; i < remap_list->size; ++i) {
     // TODO(benvanik): change encoding to avoid this branching.
@@ -37,10 +37,10 @@
     uint16_t dst_reg = remap_list->pairs[i].dst_reg;
     if (src_reg & IREE_REF_REGISTER_TYPE_BIT) {
       iree_vm_ref_retain_or_move(src_reg & IREE_REF_REGISTER_MOVE_BIT,
-                                 &regs.ref[src_reg & regs.ref_mask],
-                                 &regs.ref[dst_reg & regs.ref_mask]);
+                                 &regs_ref[src_reg & IREE_REF_REGISTER_MASK],
+                                 &regs_ref[dst_reg & IREE_REF_REGISTER_MASK]);
     } else {
-      regs.i32[dst_reg & regs.i32_mask] = regs.i32[src_reg & regs.i32_mask];
+      regs_i32[dst_reg] = regs_i32[src_reg];
     }
   }
 }
@@ -49,14 +49,14 @@
 // This can be used to eagerly release resources we don't need and reduces
 // memory consumption if used effectively prior to yields/waits.
 static void iree_vm_bytecode_dispatch_discard_registers(
-    const iree_vm_registers_t regs,
+    iree_vm_ref_t* IREE_RESTRICT regs_ref,
     const iree_vm_register_list_t* IREE_RESTRICT reg_list) {
   for (int i = 0; i < reg_list->size; ++i) {
     // TODO(benvanik): change encoding to avoid this branching.
     uint16_t reg = reg_list->registers[i];
     if ((reg & (IREE_REF_REGISTER_TYPE_BIT | IREE_REF_REGISTER_MOVE_BIT)) ==
         (IREE_REF_REGISTER_TYPE_BIT | IREE_REF_REGISTER_MOVE_BIT)) {
-      iree_vm_ref_release(&regs.ref[reg & regs.ref_mask]);
+      iree_vm_ref_release(&regs_ref[reg & IREE_REF_REGISTER_MASK]);
     }
   }
 }
@@ -65,45 +65,36 @@
 // Stack management
 //===----------------------------------------------------------------------===//
 
-static iree_vm_registers_t iree_vm_bytecode_get_register_storage(
+static inline iree_vm_registers_t iree_vm_bytecode_get_register_storage(
     iree_vm_stack_frame_t* frame) {
   const iree_vm_bytecode_frame_storage_t* stack_storage =
       (iree_vm_bytecode_frame_storage_t*)iree_vm_stack_frame_storage(frame);
-
-  // Masks indicate the valid bits of any register value within the range we
-  // have allocated in the storage. So for 4 registers we'd expect a 0b11 mask.
-  iree_vm_registers_t registers;
-  memset(&registers, 0, sizeof(registers));
-  registers.i32_mask = (uint16_t)(stack_storage->i32_register_count
-                                      ? stack_storage->i32_register_count - 1
-                                      : 0);
-  registers.ref_mask = (uint16_t)(stack_storage->ref_register_count
-                                      ? stack_storage->ref_register_count - 1
-                                      : 0);
-
-  // Register storage immediately follows the stack storage header.
-  registers.i32 =
-      (int32_t*)((uintptr_t)stack_storage + stack_storage->i32_register_offset);
-  registers.ref = (iree_vm_ref_t*)((uintptr_t)stack_storage +
-                                   stack_storage->ref_register_offset);
-
-  return registers;
+  return (iree_vm_registers_t){
+      .i32 = (int32_t*)((uintptr_t)stack_storage +
+                        stack_storage->i32_register_offset),
+      .ref = (iree_vm_ref_t*)((uintptr_t)stack_storage +
+                              stack_storage->ref_register_offset),
+  };
 }
 
 // Releases any remaining refs held in the frame storage.
 static void iree_vm_bytecode_stack_frame_cleanup(iree_vm_stack_frame_t* frame) {
-  iree_vm_registers_t regs = iree_vm_bytecode_get_register_storage(frame);
   // TODO(benvanik): allow the VM to elide this when it's known that there are
   // no more live registers.
-  for (uint16_t i = 0; i <= regs.ref_mask; ++i) {
-    iree_vm_ref_t* ref = &regs.ref[i];
+  const iree_vm_bytecode_frame_storage_t* stack_storage =
+      (iree_vm_bytecode_frame_storage_t*)iree_vm_stack_frame_storage(frame);
+  iree_vm_ref_t* refs = (iree_vm_ref_t*)((uintptr_t)stack_storage +
+                                         stack_storage->ref_register_offset);
+  for (uint16_t i = 0; i < stack_storage->ref_register_count; ++i) {
+    iree_vm_ref_t* ref = &refs[i];
     if (ref->ptr) iree_vm_ref_release(ref);
   }
 }
 
 static iree_status_t iree_vm_bytecode_function_enter(
     iree_vm_stack_t* stack, const iree_vm_function_t function,
-    iree_string_view_t cconv_results, iree_vm_stack_frame_t** out_callee_frame,
+    iree_string_view_t cconv_results,
+    iree_vm_stack_frame_t* IREE_RESTRICT* out_callee_frame,
     iree_vm_registers_t* out_callee_registers) {
   iree_vm_bytecode_module_t* module =
       (iree_vm_bytecode_module_t*)function.module->self;
@@ -118,26 +109,11 @@
   // bounds check register access. This lets us allocate the entire frame
   // (header, frame, and register storage) as a single pointer bump below.
 
-  // Round up register counts to the nearest power of 2 (if not already).
-  // This let's us use bit masks on register accesses to do bounds checking
-  // instead of more complex logic. The cost of these extra registers is only at
-  // worst 2x the required cost: so not large when thinking about the normal
-  // size of data used in an IREE app for tensors.
-  //
-  // Note that to allow the masking to work as a guard we need to ensure we at
-  // least allocate 1 register; this way an i32[reg & mask] will always point at
-  // valid memory even if mask == 0.
-  uint32_t i32_register_count = iree_math_round_up_to_pow2_u32(
-      VMMAX(1, target_descriptor->i32_register_count));
-  uint32_t ref_register_count = iree_math_round_up_to_pow2_u32(
-      VMMAX(1, target_descriptor->ref_register_count));
-  if (IREE_UNLIKELY(i32_register_count > IREE_I32_REGISTER_MASK) ||
-      IREE_UNLIKELY(ref_register_count > IREE_REF_REGISTER_MASK)) {
-    // Register count overflow. A valid compiler should never produce files that
-    // hit this.
-    return iree_make_status(IREE_STATUS_RESOURCE_EXHAUSTED,
-                            "register count overflow");
-  }
+  // We've verified all register storage prior to execution.
+  uint32_t i32_register_count = target_descriptor->i32_register_count;
+  uint32_t ref_register_count = target_descriptor->ref_register_count;
+  IREE_ASSERT_LE(i32_register_count, IREE_I32_REGISTER_MASK);
+  IREE_ASSERT_LE(ref_register_count, IREE_REF_REGISTER_MASK);
 
   // We need to align the ref register start to the natural machine
   // alignment in case the compiler is expecting that (it makes it easier to
@@ -162,8 +138,8 @@
           *out_callee_frame);
   stack_storage->cconv_results = cconv_results;
   stack_storage->i32_register_count = i32_register_count;
-  stack_storage->ref_register_count = ref_register_count;
   stack_storage->i32_register_offset = header_size;
+  stack_storage->ref_register_count = ref_register_count;
   stack_storage->ref_register_offset = header_size + i32_register_size;
   *out_callee_registers =
       iree_vm_bytecode_get_register_storage(*out_callee_frame);
@@ -181,7 +157,8 @@
 static iree_status_t iree_vm_bytecode_external_enter(
     iree_vm_stack_t* stack, const iree_vm_function_t function,
     iree_string_view_t cconv_arguments, iree_byte_span_t arguments,
-    iree_string_view_t cconv_results, iree_vm_stack_frame_t** out_callee_frame,
+    iree_string_view_t cconv_results,
+    iree_vm_stack_frame_t* IREE_RESTRICT* out_callee_frame,
     iree_vm_registers_t* out_callee_registers) {
   // Enter the bytecode function and allocate registers.
   IREE_RETURN_IF_ERROR(iree_vm_bytecode_function_enter(
@@ -199,23 +176,21 @@
       case IREE_VM_CCONV_TYPE_I32:
       case IREE_VM_CCONV_TYPE_F32: {
         uint16_t dst_reg = i32_reg++;
-        memcpy(&callee_registers.i32[dst_reg & callee_registers.i32_mask], p,
-               sizeof(int32_t));
+        memcpy(&callee_registers.i32[dst_reg], p, sizeof(int32_t));
         p += sizeof(int32_t);
       } break;
       case IREE_VM_CCONV_TYPE_I64:
       case IREE_VM_CCONV_TYPE_F64: {
         uint16_t dst_reg = i32_reg;
         i32_reg += 2;
-        memcpy(&callee_registers.i32[dst_reg & callee_registers.i32_mask], p,
-               sizeof(int64_t));
+        memcpy(&callee_registers.i32[dst_reg], p, sizeof(int64_t));
         p += sizeof(int64_t);
       } break;
       case IREE_VM_CCONV_TYPE_REF: {
         uint16_t dst_reg = ref_reg++;
         iree_vm_ref_move(
             (iree_vm_ref_t*)p,
-            &callee_registers.ref[dst_reg & callee_registers.ref_mask]);
+            &callee_registers.ref[dst_reg & IREE_REF_REGISTER_MASK]);
         p += sizeof(iree_vm_ref_t);
       } break;
     }
@@ -248,22 +223,18 @@
         break;
       case IREE_VM_CCONV_TYPE_I32:
       case IREE_VM_CCONV_TYPE_F32: {
-        memcpy(p, &callee_registers->i32[src_reg & callee_registers->i32_mask],
-               sizeof(int32_t));
+        memcpy(p, &callee_registers->i32[src_reg], sizeof(int32_t));
         p += sizeof(int32_t);
       } break;
       case IREE_VM_CCONV_TYPE_I64:
       case IREE_VM_CCONV_TYPE_F64: {
-        memcpy(
-            p,
-            &callee_registers->i32[src_reg & (callee_registers->i32_mask & ~1)],
-            sizeof(int64_t));
+        memcpy(p, &callee_registers->i32[src_reg], sizeof(int64_t));
         p += sizeof(int64_t);
       } break;
       case IREE_VM_CCONV_TYPE_REF: {
         iree_vm_ref_retain_or_move(
             src_reg & IREE_REF_REGISTER_MOVE_BIT,
-            &callee_registers->ref[src_reg & callee_registers->ref_mask],
+            &callee_registers->ref[src_reg & IREE_REF_REGISTER_MASK],
             (iree_vm_ref_t*)p);
         p += sizeof(iree_vm_ref_t);
       } break;
@@ -281,7 +252,7 @@
     iree_vm_stack_t* stack, iree_vm_module_t* module, int32_t function_ordinal,
     const iree_vm_register_list_t* IREE_RESTRICT src_reg_list,
     const iree_vm_register_list_t* IREE_RESTRICT dst_reg_list,
-    iree_vm_stack_frame_t** out_callee_frame,
+    iree_vm_stack_frame_t* IREE_RESTRICT* out_callee_frame,
     iree_vm_registers_t* out_callee_registers) {
   // Stash the destination register list for result values on the caller.
   iree_vm_bytecode_frame_storage_t* caller_storage =
@@ -317,15 +288,15 @@
     uint16_t src_reg = src_reg_list->registers[i];
     if (src_reg & IREE_REF_REGISTER_TYPE_BIT) {
       uint16_t dst_reg = ref_reg_offset++;
-      memset(&dst_regs->ref[dst_reg & dst_regs->ref_mask], 0,
+      memset(&dst_regs->ref[dst_reg & IREE_REF_REGISTER_MASK], 0,
              sizeof(iree_vm_ref_t));
-      iree_vm_ref_retain_or_move(src_reg & IREE_REF_REGISTER_MOVE_BIT,
-                                 &src_regs.ref[src_reg & src_regs.ref_mask],
-                                 &dst_regs->ref[dst_reg & dst_regs->ref_mask]);
+      iree_vm_ref_retain_or_move(
+          src_reg & IREE_REF_REGISTER_MOVE_BIT,
+          &src_regs.ref[src_reg & IREE_REF_REGISTER_MASK],
+          &dst_regs->ref[dst_reg & IREE_REF_REGISTER_MASK]);
     } else {
       uint16_t dst_reg = i32_reg_offset++;
-      dst_regs->i32[dst_reg & dst_regs->i32_mask] =
-          src_regs.i32[src_reg & src_regs.i32_mask];
+      dst_regs->i32[dst_reg] = src_regs.i32[src_reg];
     }
   }
 
@@ -339,7 +310,7 @@
     iree_vm_stack_t* stack, iree_vm_stack_frame_t* callee_frame,
     const iree_vm_registers_t callee_registers,
     const iree_vm_register_list_t* IREE_RESTRICT src_reg_list,
-    iree_vm_stack_frame_t** out_caller_frame,
+    iree_vm_stack_frame_t* IREE_RESTRICT* out_caller_frame,
     iree_vm_registers_t* out_caller_registers) {
   // Remaps registers from source to destination across frames.
   // Registers from the |src_regs| will be copied/moved to |dst_regs| with the
@@ -357,7 +328,7 @@
           caller_frame);
   const iree_vm_register_list_t* dst_reg_list =
       caller_storage->return_registers;
-  VMCHECK(src_reg_list->size <= dst_reg_list->size);
+  IREE_ASSERT_LE(src_reg_list->size, dst_reg_list->size);
   if (IREE_UNLIKELY(src_reg_list->size > dst_reg_list->size)) {
     return iree_make_status(IREE_STATUS_FAILED_PRECONDITION,
                             "src/dst reg count mismatch on internal return");
@@ -372,11 +343,10 @@
     if (src_reg & IREE_REF_REGISTER_TYPE_BIT) {
       iree_vm_ref_retain_or_move(
           src_reg & IREE_REF_REGISTER_MOVE_BIT,
-          &callee_registers.ref[src_reg & callee_registers.ref_mask],
-          &caller_registers.ref[dst_reg & caller_registers.ref_mask]);
+          &callee_registers.ref[src_reg & IREE_REF_REGISTER_MASK],
+          &caller_registers.ref[dst_reg & IREE_REF_REGISTER_MASK]);
     } else {
-      caller_registers.i32[dst_reg & caller_registers.i32_mask] =
-          callee_registers.i32[src_reg & callee_registers.i32_mask];
+      caller_registers.i32[dst_reg] = callee_registers.i32[src_reg];
     }
   }
 
@@ -401,29 +371,25 @@
         break;
       case IREE_VM_CCONV_TYPE_I32:
       case IREE_VM_CCONV_TYPE_F32: {
-        memcpy(p,
-               &caller_registers.i32[src_reg_list->registers[reg_i++] &
-                                     caller_registers.i32_mask],
+        memcpy(p, &caller_registers.i32[src_reg_list->registers[reg_i++]],
                sizeof(int32_t));
         p += sizeof(int32_t);
       } break;
       case IREE_VM_CCONV_TYPE_I64:
       case IREE_VM_CCONV_TYPE_F64: {
-        memcpy(p,
-               &caller_registers.i32[src_reg_list->registers[reg_i++] &
-                                     (caller_registers.i32_mask & ~1)],
+        memcpy(p, &caller_registers.i32[src_reg_list->registers[reg_i++]],
                sizeof(int64_t));
         p += sizeof(int64_t);
       } break;
       case IREE_VM_CCONV_TYPE_REF: {
         uint16_t src_reg = src_reg_list->registers[reg_i++];
         iree_vm_ref_assign(
-            &caller_registers.ref[src_reg & caller_registers.ref_mask],
+            &caller_registers.ref[src_reg & IREE_REF_REGISTER_MASK],
             (iree_vm_ref_t*)p);
         p += sizeof(iree_vm_ref_t);
       } break;
       case IREE_VM_CCONV_TYPE_SPAN_START: {
-        VMCHECK(segment_size_list);
+        IREE_ASSERT(segment_size_list);
         int32_t span_count = segment_size_list->registers[seg_i];
         memcpy(p, &span_count, sizeof(int32_t));
         p += sizeof(int32_t);
@@ -448,23 +414,21 @@
               case IREE_VM_CCONV_TYPE_I32:
               case IREE_VM_CCONV_TYPE_F32: {
                 memcpy(p,
-                       &caller_registers.i32[src_reg_list->registers[reg_i++] &
-                                             caller_registers.i32_mask],
+                       &caller_registers.i32[src_reg_list->registers[reg_i++]],
                        sizeof(int32_t));
                 p += sizeof(int32_t);
               } break;
               case IREE_VM_CCONV_TYPE_I64:
               case IREE_VM_CCONV_TYPE_F64: {
                 memcpy(p,
-                       &caller_registers.i32[src_reg_list->registers[reg_i++] &
-                                             (caller_registers.i32_mask & ~1)],
+                       &caller_registers.i32[src_reg_list->registers[reg_i++]],
                        sizeof(int64_t));
                 p += sizeof(int64_t);
               } break;
               case IREE_VM_CCONV_TYPE_REF: {
                 uint16_t src_reg = src_reg_list->registers[reg_i++];
                 iree_vm_ref_assign(
-                    &caller_registers.ref[src_reg & caller_registers.ref_mask],
+                    &caller_registers.ref[src_reg & IREE_REF_REGISTER_MASK],
                     (iree_vm_ref_t*)p);
                 p += sizeof(iree_vm_ref_t);
               } break;
@@ -481,7 +445,7 @@
     iree_vm_stack_t* stack, const iree_vm_function_call_t call,
     iree_string_view_t cconv_results,
     const iree_vm_register_list_t* IREE_RESTRICT dst_reg_list,
-    iree_vm_stack_frame_t** out_caller_frame,
+    iree_vm_stack_frame_t* IREE_RESTRICT* out_caller_frame,
     iree_vm_registers_t* out_caller_registers) {
   // Call external function.
   iree_status_t call_status =
@@ -517,21 +481,18 @@
         break;
       case IREE_VM_CCONV_TYPE_I32:
       case IREE_VM_CCONV_TYPE_F32:
-        memcpy(&caller_registers.i32[dst_reg & caller_registers.i32_mask], p,
-               sizeof(int32_t));
+        memcpy(&caller_registers.i32[dst_reg], p, sizeof(int32_t));
         p += sizeof(int32_t);
         break;
       case IREE_VM_CCONV_TYPE_I64:
       case IREE_VM_CCONV_TYPE_F64:
-        memcpy(
-            &caller_registers.i32[dst_reg & (caller_registers.i32_mask & ~1)],
-            p, sizeof(int64_t));
+        memcpy(&caller_registers.i32[dst_reg], p, sizeof(int64_t));
         p += sizeof(int64_t);
         break;
       case IREE_VM_CCONV_TYPE_REF:
         iree_vm_ref_move(
             (iree_vm_ref_t*)p,
-            &caller_registers.ref[dst_reg & caller_registers.ref_mask]);
+            &caller_registers.ref[dst_reg & IREE_REF_REGISTER_MASK]);
         p += sizeof(iree_vm_ref_t);
         break;
     }
@@ -546,26 +507,27 @@
     uint32_t import_ordinal, const iree_vm_bytecode_import_t** out_import) {
   *out_import = NULL;
 
+  // Ordinal has been checked as in-bounds during verification.
   import_ordinal &= 0x7FFFFFFFu;
-  if (IREE_UNLIKELY(import_ordinal >= module_state->import_count)) {
-    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                            "import ordinal %u out of range", import_ordinal);
-  }
+  IREE_ASSERT(import_ordinal < module_state->import_count);
 
   const iree_vm_bytecode_import_t* import =
       &module_state->import_table[import_ordinal];
   if (!import->function.module) {
+#if IREE_STATUS_MODE
     iree_vm_function_t decl_function;
     IREE_RETURN_IF_ERROR(iree_vm_module_lookup_function_by_ordinal(
         iree_vm_stack_current_frame(stack)->function.module,
         IREE_VM_FUNCTION_LINKAGE_IMPORT_OPTIONAL, import_ordinal,
         &decl_function));
     iree_string_view_t import_name = iree_vm_function_name(&decl_function);
-    (void)import_name;
     return iree_make_status(IREE_STATUS_NOT_FOUND,
                             "optional import `%.*s` (ordinal %u) not resolved",
                             (int)import_name.size, import_name.data,
                             import_ordinal);
+#else
+    return iree_make_status(IREE_STATUS_NOT_FOUND);
+#endif  // IREE_STATUS_MODE
   }
 
   *out_import = import;
@@ -580,7 +542,7 @@
     uint32_t import_ordinal, const iree_vm_registers_t caller_registers,
     const iree_vm_register_list_t* IREE_RESTRICT src_reg_list,
     const iree_vm_register_list_t* IREE_RESTRICT dst_reg_list,
-    iree_vm_stack_frame_t** out_caller_frame,
+    iree_vm_stack_frame_t* IREE_RESTRICT* out_caller_frame,
     iree_vm_registers_t* out_caller_registers) {
   // Prepare |call| by looking up the import information.
   const iree_vm_bytecode_import_t* import = NULL;
@@ -617,7 +579,7 @@
     const iree_vm_register_list_t* IREE_RESTRICT segment_size_list,
     const iree_vm_register_list_t* IREE_RESTRICT src_reg_list,
     const iree_vm_register_list_t* IREE_RESTRICT dst_reg_list,
-    iree_vm_stack_frame_t** out_caller_frame,
+    iree_vm_stack_frame_t* IREE_RESTRICT* out_caller_frame,
     iree_vm_registers_t* out_caller_registers) {
   // Prepare |call| by looking up the import information.
   const iree_vm_bytecode_import_t* import = NULL;
@@ -692,9 +654,10 @@
 }
 
 static iree_status_t iree_vm_bytecode_dispatch(
-    iree_vm_stack_t* stack, iree_vm_bytecode_module_t* module,
-    iree_vm_stack_frame_t* current_frame, iree_vm_registers_t regs,
-    iree_byte_span_t call_results) {
+    iree_vm_stack_t* IREE_RESTRICT stack,
+    iree_vm_bytecode_module_t* IREE_RESTRICT module,
+    iree_vm_stack_frame_t* IREE_RESTRICT current_frame,
+    iree_vm_registers_t regs, iree_byte_span_t call_results) {
   // When required emit the dispatch tables here referencing the labels we are
   // defining below.
   DEFINE_DISPATCH_TABLES();
@@ -712,8 +675,13 @@
       module->bytecode_data.data +
       module->function_descriptor_table[current_frame->function.ordinal]
           .bytecode_offset;
-  iree_vm_source_offset_t pc = current_frame->pc;
 
+  int32_t* IREE_RESTRICT regs_i32 = regs.i32;
+  IREE_BUILTIN_ASSUME_ALIGNED(regs_i32, 16);
+  iree_vm_ref_t* IREE_RESTRICT regs_ref = regs.ref;
+  IREE_BUILTIN_ASSUME_ALIGNED(regs_ref, 16);
+
+  iree_vm_source_offset_t pc = current_frame->pc;
   BEGIN_DISPATCH_CORE() {
     //===------------------------------------------------------------------===//
     // Globals
@@ -721,13 +689,7 @@
 
     DISPATCH_OP(CORE, GlobalLoadI32, {
       uint32_t byte_offset = VM_DecGlobalAttr("global");
-      if (IREE_UNLIKELY(byte_offset >=
-                        module_state->rwdata_storage.data_length)) {
-        return iree_make_status(
-            IREE_STATUS_OUT_OF_RANGE,
-            "global byte_offset out of range: %d (rwdata=%zu)", byte_offset,
-            module_state->rwdata_storage.data_length);
-      }
+      IREE_ASSERT(byte_offset + 4 <= module_state->rwdata_storage.data_length);
       int32_t* value = VM_DecResultRegI32("value");
       const int32_t global_value =
           vm_global_load_i32(module_state->rwdata_storage.data, byte_offset);
@@ -736,13 +698,7 @@
 
     DISPATCH_OP(CORE, GlobalStoreI32, {
       uint32_t byte_offset = VM_DecGlobalAttr("global");
-      if (IREE_UNLIKELY(byte_offset >=
-                        module_state->rwdata_storage.data_length)) {
-        return iree_make_status(
-            IREE_STATUS_OUT_OF_RANGE,
-            "global byte_offset out of range: %d (rwdata=%zu)", byte_offset,
-            module_state->rwdata_storage.data_length);
-      }
+      IREE_ASSERT(byte_offset + 4 <= module_state->rwdata_storage.data_length);
       int32_t value = VM_DecOperandRegI32("value");
       vm_global_store_i32(module_state->rwdata_storage.data, byte_offset,
                           value);
@@ -750,7 +706,7 @@
 
     DISPATCH_OP(CORE, GlobalLoadIndirectI32, {
       uint32_t byte_offset = VM_DecOperandRegI32("global");
-      if (IREE_UNLIKELY(byte_offset >=
+      if (IREE_UNLIKELY(byte_offset + 4 >
                         module_state->rwdata_storage.data_length)) {
         return iree_make_status(
             IREE_STATUS_OUT_OF_RANGE,
@@ -765,7 +721,7 @@
 
     DISPATCH_OP(CORE, GlobalStoreIndirectI32, {
       uint32_t byte_offset = VM_DecOperandRegI32("global");
-      if (IREE_UNLIKELY(byte_offset >=
+      if (IREE_UNLIKELY(byte_offset + 4 >
                         module_state->rwdata_storage.data_length)) {
         return iree_make_status(
             IREE_STATUS_OUT_OF_RANGE,
@@ -779,13 +735,7 @@
 
     DISPATCH_OP(CORE, GlobalLoadI64, {
       uint32_t byte_offset = VM_DecGlobalAttr("global");
-      if (IREE_UNLIKELY(byte_offset >=
-                        module_state->rwdata_storage.data_length)) {
-        return iree_make_status(
-            IREE_STATUS_OUT_OF_RANGE,
-            "global byte_offset out of range: %d (rwdata=%zu)", byte_offset,
-            module_state->rwdata_storage.data_length);
-      }
+      IREE_ASSERT(byte_offset + 8 <= module_state->rwdata_storage.data_length);
       int64_t* value = VM_DecResultRegI64("value");
       const int64_t global_value =
           vm_global_load_i64(module_state->rwdata_storage.data, byte_offset);
@@ -794,13 +744,7 @@
 
     DISPATCH_OP(CORE, GlobalStoreI64, {
       uint32_t byte_offset = VM_DecGlobalAttr("global");
-      if (IREE_UNLIKELY(byte_offset >=
-                        module_state->rwdata_storage.data_length)) {
-        return iree_make_status(
-            IREE_STATUS_OUT_OF_RANGE,
-            "global byte_offset out of range: %d (rwdata=%zu)", byte_offset,
-            module_state->rwdata_storage.data_length);
-      }
+      IREE_ASSERT(byte_offset + 8 <= module_state->rwdata_storage.data_length);
       int64_t value = VM_DecOperandRegI64("value");
       vm_global_store_i64(module_state->rwdata_storage.data, byte_offset,
                           value);
@@ -808,7 +752,7 @@
 
     DISPATCH_OP(CORE, GlobalLoadIndirectI64, {
       uint32_t byte_offset = VM_DecOperandRegI32("global");
-      if (IREE_UNLIKELY(byte_offset >=
+      if (IREE_UNLIKELY(byte_offset + 8 >
                         module_state->rwdata_storage.data_length)) {
         return iree_make_status(
             IREE_STATUS_OUT_OF_RANGE,
@@ -823,7 +767,7 @@
 
     DISPATCH_OP(CORE, GlobalStoreIndirectI64, {
       uint32_t byte_offset = VM_DecOperandRegI32("global");
-      if (IREE_UNLIKELY(byte_offset >=
+      if (IREE_UNLIKELY(byte_offset + 8 >
                         module_state->rwdata_storage.data_length)) {
         return iree_make_status(
             IREE_STATUS_OUT_OF_RANGE,
@@ -837,12 +781,7 @@
 
     DISPATCH_OP(CORE, GlobalLoadRef, {
       uint32_t global = VM_DecGlobalAttr("global");
-      if (IREE_UNLIKELY(global >= module_state->global_ref_count)) {
-        return iree_make_status(
-            IREE_STATUS_OUT_OF_RANGE,
-            "global ref ordinal out of range: %d (table=%zu)", global,
-            module_state->global_ref_count);
-      }
+      IREE_ASSERT(global < module_state->global_ref_count);
       const iree_vm_type_def_t* type_def = VM_DecTypeOf("value");
       bool result_is_move;
       iree_vm_ref_t* result = VM_DecResultRegRef("value", &result_is_move);
@@ -853,12 +792,7 @@
 
     DISPATCH_OP(CORE, GlobalStoreRef, {
       uint32_t global = VM_DecGlobalAttr("global");
-      if (IREE_UNLIKELY(global >= module_state->global_ref_count)) {
-        return iree_make_status(
-            IREE_STATUS_OUT_OF_RANGE,
-            "global ref ordinal out of range: %d (table=%zu)", global,
-            module_state->global_ref_count);
-      }
+      IREE_ASSERT(global < module_state->global_ref_count);
       const iree_vm_type_def_t* type_def = VM_DecTypeOf("value");
       bool value_is_move;
       iree_vm_ref_t* value = VM_DecOperandRegRef("value", &value_is_move);
@@ -933,12 +867,7 @@
 
     DISPATCH_OP(CORE, ConstRefRodata, {
       uint32_t rodata_ordinal = VM_DecRodataAttr("rodata");
-      if (IREE_UNLIKELY(rodata_ordinal >= module_state->rodata_ref_count)) {
-        return iree_make_status(
-            IREE_STATUS_OUT_OF_RANGE,
-            "rodata ref ordinal out of range: %d (table=%zu)", rodata_ordinal,
-            module_state->rodata_ref_count);
-      }
+      IREE_ASSERT(rodata_ordinal < module_state->rodata_ref_count);
       bool result_is_move;
       iree_vm_ref_t* result = VM_DecResultRegRef("value", &result_is_move);
       IREE_RETURN_IF_ERROR(iree_vm_ref_wrap_retain(
@@ -1062,7 +991,7 @@
       iree_host_size_t offset = VM_DecOperandRegI64HostSize("target_offset");
       iree_host_size_t length = VM_DecOperandRegI64HostSize("length");
       uint8_t value = (uint8_t)VM_DecOperandRegI32("value");
-      IREE_RETURN_IF_ERROR(vm_buffer_fill_i8(buffer, offset, length, value));
+      vm_buffer_fill_i8_inline(buffer, offset, length, value);
     });
     DISPATCH_OP(CORE, BufferFillI16, {
       bool buffer_is_move;
@@ -1075,7 +1004,7 @@
       iree_host_size_t offset = VM_DecOperandRegI64HostSize("target_offset");
       iree_host_size_t length = VM_DecOperandRegI64HostSize("length");
       uint16_t value = (uint16_t)VM_DecOperandRegI32("value");
-      IREE_RETURN_IF_ERROR(vm_buffer_fill_i16(buffer, offset, length, value));
+      vm_buffer_fill_i16_inline(buffer, offset, length, value);
     });
     DISPATCH_OP(CORE, BufferFillI32, {
       bool buffer_is_move;
@@ -1088,7 +1017,7 @@
       iree_host_size_t offset = VM_DecOperandRegI64HostSize("target_offset");
       iree_host_size_t length = VM_DecOperandRegI64HostSize("length");
       uint32_t value = VM_DecOperandRegI32("value");
-      IREE_RETURN_IF_ERROR(vm_buffer_fill_i32(buffer, offset, length, value));
+      vm_buffer_fill_i32_inline(buffer, offset, length, value);
     });
     DISPATCH_OP(CORE, BufferFillI64, {
       bool buffer_is_move;
@@ -1101,7 +1030,7 @@
       iree_host_size_t offset = VM_DecOperandRegI64HostSize("target_offset");
       iree_host_size_t length = VM_DecOperandRegI64HostSize("length");
       uint64_t value = VM_DecOperandRegI64("value");
-      IREE_RETURN_IF_ERROR(vm_buffer_fill_i64(buffer, offset, length, value));
+      vm_buffer_fill_i64_inline(buffer, offset, length, value);
     });
 
     // TODO(benvanik): rework dispatch so that the LoadI* ops can share the same
@@ -1119,7 +1048,7 @@
       }
       iree_host_size_t offset = VM_DecOperandRegI64HostSize("source_offset");
       uint32_t* result = VM_DecResultRegI32("result");
-      IREE_RETURN_IF_ERROR(vm_buffer_load_i8u(buffer, offset, result));
+      vm_buffer_load_i8u_inline(buffer, offset, result);
     });
     DISPATCH_OP(CORE, BufferLoadI8S, {
       bool buffer_is_move;
@@ -1132,7 +1061,7 @@
       }
       iree_host_size_t offset = VM_DecOperandRegI64HostSize("source_offset");
       uint32_t* result = VM_DecResultRegI32("result");
-      IREE_RETURN_IF_ERROR(vm_buffer_load_i8s(buffer, offset, result));
+      vm_buffer_load_i8s_inline(buffer, offset, result);
     });
     DISPATCH_OP(CORE, BufferLoadI16U, {
       bool buffer_is_move;
@@ -1145,7 +1074,7 @@
       }
       iree_host_size_t offset = VM_DecOperandRegI64HostSize("source_offset");
       uint32_t* result = VM_DecResultRegI32("result");
-      IREE_RETURN_IF_ERROR(vm_buffer_load_i16u(buffer, offset, result));
+      vm_buffer_load_i16u_inline(buffer, offset, result);
     });
     DISPATCH_OP(CORE, BufferLoadI16S, {
       bool buffer_is_move;
@@ -1158,7 +1087,7 @@
       }
       iree_host_size_t offset = VM_DecOperandRegI64HostSize("source_offset");
       uint32_t* result = VM_DecResultRegI32("result");
-      IREE_RETURN_IF_ERROR(vm_buffer_load_i16s(buffer, offset, result));
+      vm_buffer_load_i16s_inline(buffer, offset, result);
     });
     DISPATCH_OP(CORE, BufferLoadI32, {
       bool buffer_is_move;
@@ -1171,7 +1100,7 @@
       }
       iree_host_size_t offset = VM_DecOperandRegI64HostSize("source_offset");
       uint32_t* result = VM_DecResultRegI32("result");
-      IREE_RETURN_IF_ERROR(vm_buffer_load_i32(buffer, offset, result));
+      vm_buffer_load_i32_inline(buffer, offset, result);
     });
     DISPATCH_OP(CORE, BufferLoadI64, {
       bool buffer_is_move;
@@ -1184,7 +1113,7 @@
       }
       iree_host_size_t offset = VM_DecOperandRegI64HostSize("source_offset");
       uint64_t* result = VM_DecResultRegI64("result");
-      IREE_RETURN_IF_ERROR(vm_buffer_load_i64(buffer, offset, result));
+      vm_buffer_load_i64_inline(buffer, offset, result);
     });
 
     // TODO(benvanik): rework dispatch so that the StoreI* ops can share the
@@ -1201,7 +1130,7 @@
       }
       iree_host_size_t offset = VM_DecOperandRegI64HostSize("target_offset");
       uint8_t value = (uint8_t)VM_DecOperandRegI32("value");
-      IREE_RETURN_IF_ERROR(vm_buffer_store_i8(buffer, offset, value));
+      vm_buffer_store_i8_inline(buffer, offset, value);
     });
     DISPATCH_OP(CORE, BufferStoreI16, {
       bool buffer_is_move;
@@ -1214,7 +1143,7 @@
       }
       iree_host_size_t offset = VM_DecOperandRegI64HostSize("target_offset");
       uint16_t value = (uint16_t)VM_DecOperandRegI32("value");
-      IREE_RETURN_IF_ERROR(vm_buffer_store_i16(buffer, offset, value));
+      vm_buffer_store_i16_inline(buffer, offset, value);
     });
     DISPATCH_OP(CORE, BufferStoreI32, {
       bool buffer_is_move;
@@ -1227,7 +1156,7 @@
       }
       iree_host_size_t offset = VM_DecOperandRegI64HostSize("target_offset");
       uint32_t value = VM_DecOperandRegI32("value");
-      IREE_RETURN_IF_ERROR(vm_buffer_store_i32(buffer, offset, value));
+      vm_buffer_store_i32_inline(buffer, offset, value);
     });
     DISPATCH_OP(CORE, BufferStoreI64, {
       bool buffer_is_move;
@@ -1240,7 +1169,7 @@
       }
       iree_host_size_t offset = VM_DecOperandRegI64HostSize("target_offset");
       uint64_t value = (uint64_t)VM_DecOperandRegI64("value");
-      IREE_RETURN_IF_ERROR(vm_buffer_store_i64(buffer, offset, value));
+      vm_buffer_store_i64_inline(buffer, offset, value);
     });
 
     //===------------------------------------------------------------------===//
@@ -1444,7 +1373,7 @@
           VM_DecVariadicOperands("values");
       int32_t* result = VM_DecResultRegI32("result");
       if (index >= 0 && index < value_reg_list->size) {
-        *result = regs.i32[value_reg_list->registers[index] & regs.i32_mask];
+        *result = regs_i32[value_reg_list->registers[index]];
       } else {
         *result = default_value;
       }
@@ -1457,8 +1386,7 @@
           VM_DecVariadicOperands("values");
       int64_t* result = VM_DecResultRegI64("result");
       if (index >= 0 && index < value_reg_list->size) {
-        *result =
-            regs.i32[value_reg_list->registers[index] & (regs.i32_mask & ~1)];
+        *result = regs_i32[value_reg_list->registers[index]];
       } else {
         *result = default_value;
       }
@@ -1477,8 +1405,8 @@
       if (index >= 0 && index < value_reg_list->size) {
         bool is_move =
             value_reg_list->registers[index] & IREE_REF_REGISTER_MOVE_BIT;
-        iree_vm_ref_t* new_value =
-            &regs.ref[value_reg_list->registers[index] & regs.ref_mask];
+        iree_vm_ref_t* new_value = &regs_ref[value_reg_list->registers[index] &
+                                             IREE_REF_REGISTER_MASK];
         IREE_RETURN_IF_ERROR(iree_vm_ref_retain_or_move_checked(
             is_move, new_value, type_def->ref_type, result));
       } else {
@@ -1638,12 +1566,18 @@
     // Control flow
     //===------------------------------------------------------------------===//
 
+    // No-op in the interpreter.
+    DISPATCH_OP(CORE, Block, {});
+
     DISPATCH_OP(CORE, Branch, {
       int32_t block_pc = VM_DecBranchTarget("dest");
       const iree_vm_register_remap_list_t* remap_list =
           VM_DecBranchOperands("operands");
-      pc = block_pc;
-      iree_vm_bytecode_dispatch_remap_branch_registers(regs, remap_list);
+      pc = block_pc + IREE_VM_BLOCK_MARKER_SIZE;  // skip block marker
+      if (IREE_UNLIKELY(remap_list->size > 0)) {
+        iree_vm_bytecode_dispatch_remap_branch_registers(regs_i32, regs_ref,
+                                                         remap_list);
+      }
     });
 
     DISPATCH_OP(CORE, CondBranch, {
@@ -1655,12 +1589,17 @@
       const iree_vm_register_remap_list_t* false_remap_list =
           VM_DecBranchOperands("false_operands");
       if (condition) {
-        pc = true_block_pc;
-        iree_vm_bytecode_dispatch_remap_branch_registers(regs, true_remap_list);
+        pc = true_block_pc + IREE_VM_BLOCK_MARKER_SIZE;  // skip block marker
+        if (IREE_UNLIKELY(true_remap_list->size > 0)) {
+          iree_vm_bytecode_dispatch_remap_branch_registers(regs_i32, regs_ref,
+                                                           true_remap_list);
+        }
       } else {
-        pc = false_block_pc;
-        iree_vm_bytecode_dispatch_remap_branch_registers(regs,
-                                                         false_remap_list);
+        pc = false_block_pc + IREE_VM_BLOCK_MARKER_SIZE;  // skip block marker
+        if (IREE_UNLIKELY(false_remap_list->size > 0)) {
+          iree_vm_bytecode_dispatch_remap_branch_registers(regs_i32, regs_ref,
+                                                           false_remap_list);
+        }
       }
     });
 
@@ -1689,6 +1628,10 @@
         bytecode_data =
             module->bytecode_data.data +
             module->function_descriptor_table[function_ordinal].bytecode_offset;
+        regs_i32 = regs.i32;
+        IREE_BUILTIN_ASSUME_ALIGNED(regs_i32, 16);
+        regs_ref = regs.ref;
+        IREE_BUILTIN_ASSUME_ALIGNED(regs_ref, 16);
         pc = current_frame->pc;
       }
     });
@@ -1744,6 +1687,10 @@
           module->bytecode_data.data +
           module->function_descriptor_table[current_frame->function.ordinal]
               .bytecode_offset;
+      regs_i32 = regs.i32;
+      IREE_BUILTIN_ASSUME_ALIGNED(regs_i32, 16);
+      regs_ref = regs.ref;
+      IREE_BUILTIN_ASSUME_ALIGNED(regs_ref, 16);
       pc = current_frame->pc;
     });
 
@@ -1762,10 +1709,7 @@
       uint32_t function_ordinal = VM_DecFuncAttr("import");
       int32_t* result = VM_DecResultRegI32("result");
       uint32_t import_ordinal = function_ordinal & 0x7FFFFFFFu;
-      if (IREE_UNLIKELY(import_ordinal >= module_state->import_count)) {
-        return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                                "import ordinal out of range");
-      }
+      IREE_ASSERT(import_ordinal < module_state->import_count);
       const iree_vm_bytecode_import_t* import =
           &module_state->import_table[import_ordinal];
       *result = import->function.module != NULL ? 1 : 0;
@@ -1781,8 +1725,10 @@
       int32_t block_pc = VM_DecBranchTarget("dest");
       const iree_vm_register_remap_list_t* remap_list =
           VM_DecBranchOperands("operands");
-      iree_vm_bytecode_dispatch_remap_branch_registers(regs, remap_list);
-      current_frame->pc = block_pc;
+      iree_vm_bytecode_dispatch_remap_branch_registers(regs_i32, regs_ref,
+                                                       remap_list);
+      current_frame->pc =
+          block_pc + IREE_VM_BLOCK_MARKER_SIZE;  // skip block marker
 
       // Return magic status code indicating a yield.
       // This isn't an error, though callers not supporting coroutines will
@@ -1800,7 +1746,7 @@
       const iree_vm_register_list_t* src_reg_list =
           VM_DecVariadicOperands("operands");
       // TODO(benvanik): trace (if enabled).
-      iree_vm_bytecode_dispatch_discard_registers(regs, src_reg_list);
+      iree_vm_bytecode_dispatch_discard_registers(regs_ref, src_reg_list);
     });
 
     DISPATCH_OP(CORE, Print, {
@@ -1809,7 +1755,7 @@
       const iree_vm_register_list_t* src_reg_list =
           VM_DecVariadicOperands("operands");
       // TODO(benvanik): print.
-      iree_vm_bytecode_dispatch_discard_registers(regs, src_reg_list);
+      iree_vm_bytecode_dispatch_discard_registers(regs_ref, src_reg_list);
     });
 
     DISPATCH_OP(CORE, Break, {
@@ -1817,7 +1763,8 @@
       int32_t block_pc = VM_DecBranchTarget("dest");
       const iree_vm_register_remap_list_t* remap_list =
           VM_DecBranchOperands("operands");
-      iree_vm_bytecode_dispatch_remap_branch_registers(regs, remap_list);
+      iree_vm_bytecode_dispatch_remap_branch_registers(regs_i32, regs_ref,
+                                                       remap_list);
       pc = block_pc;
     });
 
@@ -1829,8 +1776,9 @@
       int32_t block_pc = VM_DecBranchTarget("dest");
       const iree_vm_register_remap_list_t* remap_list =
           VM_DecBranchOperands("operands");
-      iree_vm_bytecode_dispatch_remap_branch_registers(regs, remap_list);
-      pc = block_pc;
+      iree_vm_bytecode_dispatch_remap_branch_registers(regs_i32, regs_ref,
+                                                       remap_list);
+      pc = block_pc + IREE_VM_BLOCK_MARKER_SIZE;  // skip block marker
     });
 
     //===------------------------------------------------------------------===//
@@ -1845,13 +1793,8 @@
 
       DISPATCH_OP(EXT_F32, GlobalLoadF32, {
         uint32_t byte_offset = VM_DecGlobalAttr("global");
-        if (IREE_UNLIKELY(byte_offset >=
-                          module_state->rwdata_storage.data_length)) {
-          return iree_make_status(
-              IREE_STATUS_OUT_OF_RANGE,
-              "global byte_offset out of range: %d (rwdata=%zu)", byte_offset,
-              module_state->rwdata_storage.data_length);
-        }
+        IREE_ASSERT(byte_offset + 4 <=
+                    module_state->rwdata_storage.data_length);
         float* value = VM_DecResultRegF32("value");
         const float global_value =
             vm_global_load_f32(module_state->rwdata_storage.data, byte_offset);
@@ -1860,13 +1803,8 @@
 
       DISPATCH_OP(EXT_F32, GlobalStoreF32, {
         uint32_t byte_offset = VM_DecGlobalAttr("global");
-        if (IREE_UNLIKELY(byte_offset >=
-                          module_state->rwdata_storage.data_length)) {
-          return iree_make_status(
-              IREE_STATUS_OUT_OF_RANGE,
-              "global byte_offset out of range: %d (rwdata=%zu)", byte_offset,
-              module_state->rwdata_storage.data_length);
-        }
+        IREE_ASSERT(byte_offset + 4 <=
+                    module_state->rwdata_storage.data_length);
         float value = VM_DecOperandRegF32("value");
         vm_global_store_f32(module_state->rwdata_storage.data, byte_offset,
                             value);
@@ -1874,7 +1812,7 @@
 
       DISPATCH_OP(EXT_F32, GlobalLoadIndirectF32, {
         uint32_t byte_offset = VM_DecOperandRegI32("global");
-        if (IREE_UNLIKELY(byte_offset >=
+        if (IREE_UNLIKELY(byte_offset + 4 >
                           module_state->rwdata_storage.data_length)) {
           return iree_make_status(
               IREE_STATUS_OUT_OF_RANGE,
@@ -1889,7 +1827,7 @@
 
       DISPATCH_OP(EXT_F32, GlobalStoreIndirectF32, {
         uint32_t byte_offset = VM_DecOperandRegI32("global");
-        if (IREE_UNLIKELY(byte_offset >=
+        if (IREE_UNLIKELY(byte_offset + 4 >
                           module_state->rwdata_storage.data_length)) {
           return iree_make_status(
               IREE_STATUS_OUT_OF_RANGE,
@@ -1967,8 +1905,7 @@
             VM_DecVariadicOperands("values");
         float* result = VM_DecResultRegF32("result");
         if (index >= 0 && index < value_reg_list->size) {
-          *result = *((float*)&regs.i32[value_reg_list->registers[index] &
-                                        (regs.i32_mask & ~1)]);
+          *result = *((float*)&regs_i32[value_reg_list->registers[index]]);
         } else {
           *result = default_value;
         }
@@ -2098,7 +2035,7 @@
         }
         iree_host_size_t offset = VM_DecOperandRegI64HostSize("source_offset");
         float* result = VM_DecResultRegF32("result");
-        IREE_RETURN_IF_ERROR(vm_buffer_load_f32(buffer, offset, result));
+        vm_buffer_load_f32_inline(buffer, offset, result);
       });
 
       DISPATCH_OP(EXT_F32, BufferStoreF32, {
@@ -2112,7 +2049,7 @@
         }
         iree_host_size_t offset = VM_DecOperandRegI64HostSize("target_offset");
         float value = VM_DecOperandRegF32("value");
-        IREE_RETURN_IF_ERROR(vm_buffer_store_f32(buffer, offset, value));
+        vm_buffer_store_f32_inline(buffer, offset, value);
       });
     }
     END_DISPATCH_PREFIX();
diff --git a/runtime/src/iree/vm/bytecode/dispatch_util.h b/runtime/src/iree/vm/bytecode/dispatch_util.h
index 503a372..d460df2 100644
--- a/runtime/src/iree/vm/bytecode/dispatch_util.h
+++ b/runtime/src/iree/vm/bytecode/dispatch_util.h
@@ -10,11 +10,9 @@
 #include <assert.h>
 #include <string.h>
 
-#include "iree/base/alignment.h"
-#include "iree/base/config.h"
-#include "iree/base/target_platform.h"
-#include "iree/vm/bytecode/generated/op_table.h"
+#include "iree/base/api.h"
 #include "iree/vm/bytecode/module_impl.h"
+#include "iree/vm/bytecode/utils/isa.h"
 
 //===----------------------------------------------------------------------===//
 // Shared data structures
@@ -63,14 +61,8 @@
 
 // Pointers to typed register storage.
 typedef struct iree_vm_registers_t {
-  // Ordinal mask defining which ordinal bits are valid. All i32 indexing must
-  // be ANDed with this mask.
-  uint16_t i32_mask;
   // 16-byte aligned i32 register array.
   int32_t* i32;
-  // Ordinal mask defining which ordinal bits are valid. All ref indexing must
-  // be ANDed with this mask.
-  uint16_t ref_mask;
   // Naturally aligned ref register array.
   iree_vm_ref_t* ref;
 } iree_vm_registers_t;
@@ -86,30 +78,14 @@
   // will be stored by callees upon return.
   const iree_vm_register_list_t* return_registers;
 
-  // Counts of each register type rounded up to the next power of two.
-  iree_host_size_t i32_register_count;
-  iree_host_size_t ref_register_count;
-
-  // Relative byte offsets from the head of this struct.
-  iree_host_size_t i32_register_offset;
-  iree_host_size_t ref_register_offset;
+  // Counts of each register type and their relative byte offsets from the head
+  // of this struct.
+  uint32_t i32_register_count;
+  uint32_t i32_register_offset;
+  uint32_t ref_register_count;
+  uint32_t ref_register_offset;
 } iree_vm_bytecode_frame_storage_t;
 
-// Interleaved src-dst register sets for branch register remapping.
-// This structure is an overlay for the bytecode that is serialized in a
-// matching format.
-typedef struct iree_vm_register_remap_list_t {
-  uint16_t size;
-  struct pair {
-    uint16_t src_reg;
-    uint16_t dst_reg;
-  } pairs[];
-} iree_vm_register_remap_list_t;
-static_assert(iree_alignof(iree_vm_register_remap_list_t) == 2,
-              "Expecting byte alignment (to avoid padding)");
-static_assert(offsetof(iree_vm_register_remap_list_t, pairs) == 2,
-              "Expect no padding in the struct");
-
 // Maps a type ID to a type def with clamping for out of bounds values.
 static inline const iree_vm_type_def_t* iree_vm_map_type(
     iree_vm_bytecode_module_t* module, int32_t type_id) {
@@ -147,27 +123,6 @@
 #define IREE_DISPATCH_MODE_SWITCH 1
 #endif  // IREE_VM_BYTECODE_DISPATCH_COMPUTED_GOTO_ENABLE
 
-#ifndef NDEBUG
-#define VMCHECK(expr) assert(expr)
-#else
-#define VMCHECK(expr)
-#endif  // NDEBUG
-
-//===----------------------------------------------------------------------===//
-// Bytecode data reading with little-/big-endian support
-//===----------------------------------------------------------------------===//
-
-static const int kRegSize = sizeof(uint16_t);
-
-// Bytecode data access macros for reading values of a given type from a byte
-// offset within the current function.
-#define OP_I8(i) iree_unaligned_load_le((uint8_t*)&bytecode_data[pc + (i)])
-#define OP_I16(i) iree_unaligned_load_le((uint16_t*)&bytecode_data[pc + (i)])
-#define OP_I32(i) iree_unaligned_load_le((uint32_t*)&bytecode_data[pc + (i)])
-#define OP_I64(i) iree_unaligned_load_le((uint64_t*)&bytecode_data[pc + (i)])
-#define OP_F32(i) iree_unaligned_load_le((float*)&bytecode_data[pc + (i)])
-#define OP_F64(i) iree_unaligned_load_le((double*)&bytecode_data[pc + (i)])
-
 //===----------------------------------------------------------------------===//
 // Utilities matching the tablegen op encoding scheme
 //===----------------------------------------------------------------------===//
@@ -177,9 +132,6 @@
 // Each macro will increment the pc by the number of bytes read and as such must
 // be called in the same order the values are encoded.
 
-#define VM_AlignPC(pc, alignment) \
-  (pc) = ((pc) + ((alignment)-1)) & ~((alignment)-1)
-
 #define VM_DecConstI8(name) \
   OP_I8(0);                 \
   ++pc;
@@ -195,7 +147,6 @@
 #define VM_DecConstF64(name) \
   OP_F64(0);                 \
   pc += 8;
-#define VM_DecOpcode(opcode) VM_DecConstI8(#opcode)
 #define VM_DecFuncAttr(name) VM_DecConstI32(name)
 #define VM_DecGlobalAttr(name) VM_DecConstI32(name)
 #define VM_DecRodataAttr(name) VM_DecConstI32(name)
@@ -216,58 +167,62 @@
   VM_DecBranchOperandsImpl(bytecode_data, &pc)
 static inline const iree_vm_register_remap_list_t* VM_DecBranchOperandsImpl(
     const uint8_t* IREE_RESTRICT bytecode_data, iree_vm_source_offset_t* pc) {
-  VM_AlignPC(*pc, kRegSize);
+  VM_AlignPC(*pc, IREE_REGISTER_ORDINAL_SIZE);
   const iree_vm_register_remap_list_t* list =
       (const iree_vm_register_remap_list_t*)&bytecode_data[*pc];
-  *pc = *pc + kRegSize + list->size * 2 * kRegSize;
+  *pc = *pc + IREE_REGISTER_ORDINAL_SIZE +
+        list->size * 2 * IREE_REGISTER_ORDINAL_SIZE;
   return list;
 }
-#define VM_DecOperandRegI32(name)      \
-  regs.i32[OP_I16(0) & regs.i32_mask]; \
-  pc += kRegSize;
-#define VM_DecOperandRegI64(name)                           \
-  *((int64_t*)&regs.i32[OP_I16(0) & (regs.i32_mask & ~1)]); \
-  pc += kRegSize;
+#define VM_DecOperandRegI32(name) \
+  regs_i32[OP_I16(0)];            \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_DecOperandRegI64(name)    \
+  *((int64_t*)&regs_i32[OP_I16(0)]); \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
 #define VM_DecOperandRegI64HostSize(name) \
   (iree_host_size_t) VM_DecOperandRegI64(name)
-#define VM_DecOperandRegF32(name)                  \
-  *((float*)&regs.i32[OP_I16(0) & regs.i32_mask]); \
-  pc += kRegSize;
-#define VM_DecOperandRegF64(name)                          \
-  *((double*)&regs.i32[OP_I16(0) & (regs.i32_mask & ~1)]); \
-  pc += kRegSize;
+#define VM_DecOperandRegF32(name)  \
+  *((float*)&regs_i32[OP_I16(0)]); \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_DecOperandRegF64(name)   \
+  *((double*)&regs_i32[OP_I16(0)]); \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
 #define VM_DecOperandRegRef(name, out_is_move)                      \
-  &regs.ref[OP_I16(0) & regs.ref_mask];                             \
+  &regs_ref[OP_I16(0) & IREE_REF_REGISTER_MASK];                    \
   *(out_is_move) = 0; /*= OP_I16(0) & IREE_REF_REGISTER_MOVE_BIT;*/ \
-  pc += kRegSize;
+  pc += IREE_REGISTER_ORDINAL_SIZE;
 #define VM_DecVariadicOperands(name) \
   VM_DecVariadicOperandsImpl(bytecode_data, &pc)
 static inline const iree_vm_register_list_t* VM_DecVariadicOperandsImpl(
     const uint8_t* IREE_RESTRICT bytecode_data, iree_vm_source_offset_t* pc) {
-  VM_AlignPC(*pc, kRegSize);
+  VM_AlignPC(*pc, IREE_REGISTER_ORDINAL_SIZE);
   const iree_vm_register_list_t* list =
       (const iree_vm_register_list_t*)&bytecode_data[*pc];
-  *pc = *pc + kRegSize + list->size * kRegSize;
+  *pc = *pc + IREE_REGISTER_ORDINAL_SIZE +
+        list->size * IREE_REGISTER_ORDINAL_SIZE;
   return list;
 }
-#define VM_DecResultRegI32(name)        \
-  &regs.i32[OP_I16(0) & regs.i32_mask]; \
-  pc += kRegSize;
-#define VM_DecResultRegI64(name)                           \
-  ((int64_t*)&regs.i32[OP_I16(0) & (regs.i32_mask & ~1)]); \
-  pc += kRegSize;
-#define VM_DecResultRegF32(name)                  \
-  ((float*)&regs.i32[OP_I16(0) & regs.i32_mask]); \
-  pc += kRegSize;
-#define VM_DecResultRegF64(name)                          \
-  ((double*)&regs.i32[OP_I16(0) & (regs.i32_mask & ~1)]); \
-  pc += kRegSize;
+#define VM_DecResultRegI32(name) \
+  &regs_i32[OP_I16(0)];          \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_DecResultRegI64(name)    \
+  ((int64_t*)&regs_i32[OP_I16(0)]); \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_DecResultRegF32(name)  \
+  ((float*)&regs_i32[OP_I16(0)]); \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_DecResultRegF64(name)   \
+  ((double*)&regs_i32[OP_I16(0)]); \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
 #define VM_DecResultRegRef(name, out_is_move)                       \
-  &regs.ref[OP_I16(0) & regs.ref_mask];                             \
+  &regs_ref[OP_I16(0) & IREE_REF_REGISTER_MASK];                    \
   *(out_is_move) = 0; /*= OP_I16(0) & IREE_REF_REGISTER_MOVE_BIT;*/ \
-  pc += kRegSize;
+  pc += IREE_REGISTER_ORDINAL_SIZE;
 #define VM_DecVariadicResults(name) VM_DecVariadicOperands(name)
 
+#define IREE_VM_BLOCK_MARKER_SIZE 1
+
 //===----------------------------------------------------------------------===//
 // Dispatch table structure
 //===----------------------------------------------------------------------===//
@@ -276,13 +231,6 @@
 // doesn't support it, though, and there may be other targets (like wasm) that
 // can only handle the switch-based approach.
 
-// Bytecode data -offset used when looking for the start of the currently
-// dispatched instruction: `instruction_start = pc - OFFSET`
-#define VM_PC_OFFSET_CORE 1
-#define VM_PC_OFFSET_EXT_I32 2
-#define VM_PC_OFFSET_EXT_F32 2
-#define VM_PC_OFFSET_EXT_F64 2
-
 #if defined(IREE_DISPATCH_MODE_COMPUTED_GOTO)
 
 // Dispatch table mapping 1:1 with bytecode ops.
@@ -334,20 +282,20 @@
 
 #define DISPATCH_UNHANDLED_CORE()                                           \
   _dispatch_unhandled : {                                                   \
-    VMCHECK(0);                                                             \
+    IREE_ASSERT(0);                                                         \
     return iree_make_status(IREE_STATUS_UNIMPLEMENTED, "unhandled opcode"); \
   }
 #define UNHANDLED_DISPATCH_PREFIX(op_name, ext)                    \
   _dispatch_CORE_##op_name : {                                     \
-    VMCHECK(0);                                                    \
+    IREE_ASSERT(0);                                                \
     return iree_make_status(IREE_STATUS_UNIMPLEMENTED,             \
                             "unhandled dispatch extension " #ext); \
   }
 
-#define DISPATCH_OP(ext, op_name, body)                          \
-  _dispatch_##ext##_##op_name:;                                  \
-  IREE_DISPATCH_TRACE_INSTRUCTION(VM_PC_OFFSET_##ext, #op_name); \
-  body;                                                          \
+#define DISPATCH_OP(ext, op_name, body)                               \
+  _dispatch_##ext##_##op_name:;                                       \
+  IREE_DISPATCH_TRACE_INSTRUCTION(IREE_VM_PC_OFFSET_##ext, #op_name); \
+  body;                                                               \
   goto* kDispatchTable_CORE[bytecode_data[pc++]];
 
 #define BEGIN_DISPATCH_PREFIX(op_name, ext)                                   \
@@ -367,23 +315,25 @@
 
 #define DEFINE_DISPATCH_TABLES()
 
-#define DISPATCH_UNHANDLED_CORE()                      \
-  default: {                                           \
-    VMCHECK(0);                                        \
-    return iree_make_status(IREE_STATUS_UNIMPLEMENTED, \
-                            "unhandled core opcode");  \
+#define DISPATCH_UNHANDLED_CORE()                         \
+  default: {                                              \
+    IREE_ASSERT(0);                                       \
+    IREE_BUILTIN_UNREACHABLE(); /* ok because verified */ \
+    return iree_make_status(IREE_STATUS_UNIMPLEMENTED,    \
+                            "unhandled core opcode");     \
   }
 #define UNHANDLED_DISPATCH_PREFIX(op_name, ext)                    \
   case IREE_VM_OP_CORE_##op_name: {                                \
-    VMCHECK(0);                                                    \
+    IREE_ASSERT(0);                                                \
+    IREE_BUILTIN_UNREACHABLE(); /* ok because verified */          \
     return iree_make_status(IREE_STATUS_UNIMPLEMENTED,             \
                             "unhandled dispatch extension " #ext); \
   }
 
-#define DISPATCH_OP(ext, op_name, body)                            \
-  case IREE_VM_OP_##ext##_##op_name: {                             \
-    IREE_DISPATCH_TRACE_INSTRUCTION(VM_PC_OFFSET_##ext, #op_name); \
-    body;                                                          \
+#define DISPATCH_OP(ext, op_name, body)                                 \
+  case IREE_VM_OP_##ext##_##op_name: {                                  \
+    IREE_DISPATCH_TRACE_INSTRUCTION(IREE_VM_PC_OFFSET_##ext, #op_name); \
+    body;                                                               \
   } break;
 
 #define BEGIN_DISPATCH_PREFIX(op_name, ext) \
diff --git a/runtime/src/iree/vm/bytecode/module.c b/runtime/src/iree/vm/bytecode/module.c
index b122f73..4d88f4c 100644
--- a/runtime/src/iree/vm/bytecode/module.c
+++ b/runtime/src/iree/vm/bytecode/module.c
@@ -10,146 +10,10 @@
 #include <stdint.h>
 #include <string.h>
 
-#include "iree/base/api.h"
 #include "iree/base/tracing.h"
-#include "iree/vm/api.h"
+#include "iree/vm/bytecode/archive.h"
 #include "iree/vm/bytecode/module_impl.h"
-
-// Alignment applied to each segment of the archive.
-// All embedded file contents (FlatBuffers, rodata, etc) are aligned to this
-// boundary.
-#define IREE_VM_ARCHIVE_SEGMENT_ALIGNMENT 64
-
-// ZIP local file header (comes immediately before each file in the archive).
-// In order to find the starting offset of the FlatBuffer in a polyglot archive
-// we need to parse this given the variable-length nature of it (we want to
-// be robust to file name and alignment changes).
-//
-// NOTE: all fields are little-endian.
-// NOTE: we don't care about the actual module size here; since we support
-//       streaming archives trying to recover it would require much more
-//       involved processing (we'd need to reference the central directory).
-//       If we wanted to support users repacking ZIPs we'd probably want to
-//       rewrite everything as we store offsets in the FlatBuffer that are
-//       difficult to update after the archive has been produced.
-#define ZIP_LOCAL_FILE_HEADER_SIGNATURE 0x04034B50u
-#if defined(IREE_COMPILER_MSVC)
-#pragma pack(push, 1)
-#endif  // IREE_COMPILER_MSVC
-typedef struct {
-  uint32_t signature;  // ZIP_LOCAL_FILE_HEADER_SIGNATURE
-  uint16_t version;
-  uint16_t general_purpose_flag;
-  uint16_t compression_method;
-  uint16_t last_modified_time;
-  uint16_t last_modified_date;
-  uint32_t crc32;              // 0 for us
-  uint32_t compressed_size;    // 0 for us
-  uint32_t uncompressed_size;  // 0 for us
-  uint16_t file_name_length;
-  uint16_t extra_field_length;
-  // file name (variable size)
-  // extra field (variable size)
-} IREE_ATTRIBUTE_PACKED zip_local_file_header_t;
-#if defined(IREE_COMPILER_MSVC)
-#pragma pack(pop)
-#endif  // IREE_COMPILER_MSVC
-static_assert(sizeof(zip_local_file_header_t) == 30, "bad packing");
-#if !defined(IREE_ENDIANNESS_LITTLE) || !IREE_ENDIANNESS_LITTLE
-#error "little endian required for zip header parsing"
-#endif  // IREE_ENDIANNESS_LITTLE
-
-// Strips any ZIP local file header from |contents| and stores the remaining
-// range in |out_stripped|.
-static iree_status_t iree_vm_bytecode_module_strip_zip_header(
-    iree_const_byte_span_t contents, iree_const_byte_span_t* out_stripped) {
-  // Ensure there's at least some bytes we can check for the header.
-  // Since we're only looking to strip zip stuff here we can check on that.
-  if (!contents.data ||
-      contents.data_length < sizeof(zip_local_file_header_t)) {
-    memmove(out_stripped, &contents, sizeof(contents));
-    return iree_ok_status();
-  }
-
-  // Check to see if there's a zip local header signature.
-  // For a compliant zip file this is expected to start at offset 0.
-  const zip_local_file_header_t* header =
-      (const zip_local_file_header_t*)contents.data;
-  if (header->signature != ZIP_LOCAL_FILE_HEADER_SIGNATURE) {
-    // No signature found, probably not a ZIP.
-    memmove(out_stripped, &contents, sizeof(contents));
-    return iree_ok_status();
-  }
-
-  // Compute the starting offset of the file.
-  // Note that we still don't know (or care) if it's the file we want; actual
-  // FlatBuffer verification happens later on.
-  uint32_t offset =
-      sizeof(*header) + header->file_name_length + header->extra_field_length;
-  if (offset > contents.data_length) {
-    // Is a ZIP but doesn't have enough data; error out with something more
-    // useful than the FlatBuffer verification failing later on given that here
-    // we know this isn't a FlatBuffer.
-    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                            "archive self-reports as a zip but does not have "
-                            "enough data to contain a module");
-  }
-
-  *out_stripped = iree_make_const_byte_span(contents.data + offset,
-                                            contents.data_length - offset);
-  return iree_ok_status();
-}
-
-IREE_API_EXPORT iree_status_t iree_vm_bytecode_module_parse_header(
-    iree_const_byte_span_t archive_contents,
-    iree_const_byte_span_t* out_flatbuffer_contents,
-    iree_host_size_t* out_rodata_offset) {
-  // Slice off any polyglot zip header we have prior to the base of the module.
-  iree_const_byte_span_t module_contents = iree_const_byte_span_empty();
-  IREE_RETURN_IF_ERROR(iree_vm_bytecode_module_strip_zip_header(
-      archive_contents, &module_contents));
-
-  // Verify there's enough data to safely check the FlatBuffer header.
-  if (!module_contents.data || module_contents.data_length < 16) {
-    return iree_make_status(
-        IREE_STATUS_INVALID_ARGUMENT,
-        "FlatBuffer data is not present or less than 16 bytes (%zu total)",
-        module_contents.data_length);
-  }
-
-  // Read the size prefix from the head of the module contents; this should be
-  // a 4 byte value indicating the total size of the FlatBuffer data.
-  size_t length_prefix = 0;
-  flatbuffers_read_size_prefix((void*)module_contents.data, &length_prefix);
-
-  // Verify the length prefix is within bounds (always <= the remaining module
-  // bytes).
-  size_t length_remaining =
-      module_contents.data_length - sizeof(flatbuffers_uoffset_t);
-  if (length_prefix > length_remaining) {
-    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                            "FlatBuffer length prefix out of bounds (prefix is "
-                            "%zu but only %zu available)",
-                            length_prefix, length_remaining);
-  }
-
-  // Form the range of bytes containing just the FlatBuffer data.
-  iree_const_byte_span_t flatbuffer_contents = iree_make_const_byte_span(
-      module_contents.data + sizeof(flatbuffers_uoffset_t), length_prefix);
-
-  if (out_flatbuffer_contents) {
-    *out_flatbuffer_contents = flatbuffer_contents;
-  }
-  if (out_rodata_offset) {
-    // rodata begins immediately following the FlatBuffer in memory.
-    iree_host_size_t rodata_offset = iree_host_align(
-        (iree_host_size_t)(flatbuffer_contents.data - archive_contents.data) +
-            length_prefix,
-        IREE_VM_ARCHIVE_SEGMENT_ALIGNMENT);
-    *out_rodata_offset = rodata_offset;
-  }
-  return iree_ok_status();
-}
+#include "iree/vm/bytecode/verifier.h"
 
 // Perform an strcmp between a FlatBuffers string and an IREE string view.
 static bool iree_vm_flatbuffer_strcmp(flatbuffers_string_t lhs,
@@ -236,177 +100,6 @@
   return status;
 }
 
-// Verifies the structure of the FlatBuffer so that we can avoid doing so during
-// runtime. There are still some conditions we must be aware of (such as omitted
-// names on functions with internal linkage), however we shouldn't need to
-// bounds check anything within the FlatBuffer after this succeeds.
-static iree_status_t iree_vm_bytecode_module_flatbuffer_verify(
-    iree_const_byte_span_t archive_contents,
-    iree_const_byte_span_t flatbuffer_contents,
-    iree_host_size_t archive_rodata_offset) {
-  // Run flatcc generated verification. This ensures all pointers are in-bounds
-  // and that we can safely walk the file, but not that the actual contents of
-  // the FlatBuffer meet our expectations.
-  int verify_ret = iree_vm_BytecodeModuleDef_verify_as_root(
-      flatbuffer_contents.data, flatbuffer_contents.data_length);
-  if (verify_ret != flatcc_verify_ok) {
-    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                            "FlatBuffer verification failed: %s",
-                            flatcc_verify_error_string(verify_ret));
-  }
-
-  iree_vm_BytecodeModuleDef_table_t module_def =
-      iree_vm_BytecodeModuleDef_as_root(flatbuffer_contents.data);
-
-  flatbuffers_string_t name = iree_vm_BytecodeModuleDef_name(module_def);
-  if (!flatbuffers_string_len(name)) {
-    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                            "module missing name field");
-  }
-
-  iree_vm_TypeDef_vec_t types = iree_vm_BytecodeModuleDef_types(module_def);
-  for (size_t i = 0; i < iree_vm_TypeDef_vec_len(types); ++i) {
-    iree_vm_TypeDef_table_t type_def = iree_vm_TypeDef_vec_at(types, i);
-    if (!type_def) {
-      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                              "types[%zu] missing body", i);
-    }
-    flatbuffers_string_t full_name = iree_vm_TypeDef_full_name(type_def);
-    if (flatbuffers_string_len(full_name) <= 0) {
-      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                              "types[%zu] missing name", i);
-    }
-  }
-
-  iree_vm_RodataSegmentDef_vec_t rodata_segments =
-      iree_vm_BytecodeModuleDef_rodata_segments(module_def);
-  for (size_t i = 0; i < iree_vm_RodataSegmentDef_vec_len(rodata_segments);
-       ++i) {
-    iree_vm_RodataSegmentDef_table_t segment =
-        iree_vm_RodataSegmentDef_vec_at(rodata_segments, i);
-    if (iree_vm_RodataSegmentDef_embedded_data_is_present(segment)) {
-      continue;  // embedded data is verified by FlatBuffers
-    }
-    uint64_t segment_offset =
-        iree_vm_RodataSegmentDef_external_data_offset(segment);
-    uint64_t segment_length =
-        iree_vm_RodataSegmentDef_external_data_length(segment);
-    uint64_t segment_end =
-        archive_rodata_offset + segment_offset + segment_length;
-    if (segment_end > archive_contents.data_length) {
-      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                              "rodata[%zu] external reference out of range", i);
-    }
-  }
-
-  iree_vm_ModuleDependencyDef_vec_t dependencies =
-      iree_vm_BytecodeModuleDef_dependencies(module_def);
-  for (size_t i = 0; i < iree_vm_ModuleDependencyDef_vec_len(dependencies);
-       ++i) {
-    iree_vm_ModuleDependencyDef_table_t dependency_def =
-        iree_vm_ModuleDependencyDef_vec_at(dependencies, i);
-    flatbuffers_string_t module_name =
-        iree_vm_ModuleDependencyDef_name(dependency_def);
-    if (flatbuffers_string_len(module_name) == 0) {
-      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                              "dependencies[%zu] has no module name", i);
-    }
-  }
-
-  iree_vm_ImportFunctionDef_vec_t imported_functions =
-      iree_vm_BytecodeModuleDef_imported_functions(module_def);
-  iree_vm_ExportFunctionDef_vec_t exported_functions =
-      iree_vm_BytecodeModuleDef_exported_functions(module_def);
-  iree_vm_FunctionDescriptor_vec_t function_descriptors =
-      iree_vm_BytecodeModuleDef_function_descriptors(module_def);
-
-  for (size_t i = 0; i < iree_vm_ImportFunctionDef_vec_len(imported_functions);
-       ++i) {
-    iree_vm_ImportFunctionDef_table_t import_def =
-        iree_vm_ImportFunctionDef_vec_at(imported_functions, i);
-    if (!import_def) {
-      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                              "imports[%zu] missing body", i);
-    }
-    flatbuffers_string_t full_name =
-        iree_vm_ImportFunctionDef_full_name(import_def);
-    if (!flatbuffers_string_len(full_name)) {
-      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                              "imports[%zu] missing full_name", i);
-    }
-  }
-
-  for (size_t i = 0; i < iree_vm_ExportFunctionDef_vec_len(exported_functions);
-       ++i) {
-    iree_vm_ExportFunctionDef_table_t export_def =
-        iree_vm_ExportFunctionDef_vec_at(exported_functions, i);
-    if (!export_def) {
-      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                              "exports[%zu] missing body", i);
-    }
-    flatbuffers_string_t local_name =
-        iree_vm_ExportFunctionDef_local_name(export_def);
-    if (!flatbuffers_string_len(local_name)) {
-      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                              "exports[%zu] missing local_name", i);
-    }
-    iree_host_size_t internal_ordinal =
-        iree_vm_ExportFunctionDef_internal_ordinal(export_def);
-    if (internal_ordinal >=
-        iree_vm_FunctionDescriptor_vec_len(function_descriptors)) {
-      return iree_make_status(
-          IREE_STATUS_INVALID_ARGUMENT,
-          "exports[%zu] internal_ordinal out of bounds (0 < %zu < %zu)", i,
-          internal_ordinal,
-          iree_vm_FunctionDescriptor_vec_len(function_descriptors));
-    }
-  }
-
-  // Verify that we can properly handle the bytecode embedded in the module.
-  // We require that major versions match and allow loading of older minor
-  // versions (we keep changes backwards-compatible).
-  const uint32_t bytecode_version =
-      iree_vm_BytecodeModuleDef_bytecode_version(module_def);
-  const uint32_t bytecode_version_major = bytecode_version >> 16;
-  const uint32_t bytecode_version_minor = bytecode_version & 0xFFFF;
-  if ((bytecode_version_major != IREE_VM_BYTECODE_VERSION_MAJOR) ||
-      (bytecode_version_minor > IREE_VM_BYTECODE_VERSION_MINOR)) {
-    return iree_make_status(
-        IREE_STATUS_INVALID_ARGUMENT,
-        "bytecode version mismatch; runtime supports %d.%d, module has %d.%d",
-        IREE_VM_BYTECODE_VERSION_MAJOR, IREE_VM_BYTECODE_VERSION_MINOR,
-        bytecode_version_major, bytecode_version_minor);
-  }
-
-  flatbuffers_uint8_vec_t bytecode_data =
-      iree_vm_BytecodeModuleDef_bytecode_data(module_def);
-  for (size_t i = 0;
-       i < iree_vm_FunctionDescriptor_vec_len(function_descriptors); ++i) {
-    iree_vm_FunctionDescriptor_struct_t function_descriptor =
-        iree_vm_FunctionDescriptor_vec_at(function_descriptors, i);
-    if (function_descriptor->bytecode_offset < 0 ||
-        function_descriptor->bytecode_offset +
-                function_descriptor->bytecode_length >
-            flatbuffers_uint8_vec_len(bytecode_data)) {
-      return iree_make_status(
-          IREE_STATUS_INVALID_ARGUMENT,
-          "functions[%zu] descriptor bytecode span out of range (0 < %d < %zu)",
-          i, function_descriptor->bytecode_offset,
-          flatbuffers_uint8_vec_len(bytecode_data));
-    }
-    if (function_descriptor->i32_register_count > IREE_I32_REGISTER_COUNT ||
-        function_descriptor->ref_register_count > IREE_REF_REGISTER_COUNT) {
-      return iree_make_status(
-          IREE_STATUS_INVALID_ARGUMENT,
-          "functions[%zu] descriptor register count out of range", i);
-    }
-
-    // TODO(benvanik): run bytecode verifier on contents.
-  }
-
-  return iree_ok_status();
-}
-
 static iree_status_t iree_vm_bytecode_map_internal_ordinal(
     iree_vm_bytecode_module_t* module, iree_vm_function_t function,
     uint16_t* out_ordinal,
@@ -427,7 +120,8 @@
     iree_vm_ExportFunctionDef_table_t function_def =
         iree_vm_ExportFunctionDef_vec_at(exported_functions, function.ordinal);
     ordinal = iree_vm_ExportFunctionDef_internal_ordinal(function_def);
-    signature_def = iree_vm_ExportFunctionDef_signature(function_def);
+    signature_def = iree_vm_FunctionSignatureDef_vec_at(
+        iree_vm_BytecodeModuleDef_function_signatures(module->def), ordinal);
   } else {
     // TODO(benvanik): support querying the internal functions, which could be
     // useful for debugging. Or maybe we just drop them forever?
@@ -653,12 +347,16 @@
     iree_vm_ExportFunctionDef_table_t export_def =
         iree_vm_ExportFunctionDef_vec_at(exported_functions, ordinal);
     name = iree_vm_ExportFunctionDef_local_name(export_def);
-    signature = iree_vm_ExportFunctionDef_signature(export_def);
+    signature = iree_vm_FunctionSignatureDef_vec_at(
+        iree_vm_BytecodeModuleDef_function_signatures(module->def),
+        iree_vm_ExportFunctionDef_internal_ordinal(export_def));
   } else if (linkage == IREE_VM_FUNCTION_LINKAGE_INTERNAL) {
 #if IREE_VM_BACKTRACE_ENABLE
     name = iree_vm_bytecode_module_lookup_internal_function_name(module->def,
                                                                  ordinal);
 #endif  // IREE_VM_BACKTRACE_ENABLE
+    signature = iree_vm_FunctionSignatureDef_vec_at(
+        iree_vm_BytecodeModuleDef_function_signatures(module->def), ordinal);
   }
 
   if (out_function) {
@@ -692,6 +390,8 @@
   iree_vm_bytecode_module_t* module = (iree_vm_bytecode_module_t*)self;
   iree_vm_ExportFunctionDef_vec_t exported_functions =
       iree_vm_BytecodeModuleDef_exported_functions(module->def);
+  iree_vm_FunctionSignatureDef_vec_t function_signatures =
+      iree_vm_BytecodeModuleDef_function_signatures(module->def);
 
   if (ordinal >= iree_vm_ExportFunctionDef_vec_len(exported_functions)) {
     return iree_make_status(
@@ -703,7 +403,9 @@
   iree_vm_ExportFunctionDef_table_t function_def =
       iree_vm_ExportFunctionDef_vec_at(exported_functions, ordinal);
   iree_vm_FunctionSignatureDef_table_t signature_def =
-      iree_vm_ExportFunctionDef_signature(function_def);
+      iree_vm_FunctionSignatureDef_vec_at(
+          function_signatures,
+          iree_vm_ExportFunctionDef_internal_ordinal(function_def));
   if (!signature_def) {
     return iree_make_status(
         IREE_STATUS_NOT_FOUND,
@@ -1130,7 +832,7 @@
   iree_const_byte_span_t flatbuffer_contents = iree_const_byte_span_empty();
   iree_host_size_t archive_rodata_offset = 0;
   IREE_RETURN_AND_END_ZONE_IF_ERROR(
-      z0, iree_vm_bytecode_module_parse_header(
+      z0, iree_vm_bytecode_archive_parse_header(
               archive_contents, &flatbuffer_contents, &archive_rodata_offset));
 
   IREE_TRACE_ZONE_BEGIN_NAMED(z1, "iree_vm_bytecode_module_flatbuffer_verify");
@@ -1210,7 +912,23 @@
   module->interface.begin_call = iree_vm_bytecode_module_begin_call;
   module->interface.resume_call = iree_vm_bytecode_module_resume_call;
 
-  *out_module = &module->interface;
+  // Verify functions in the module now that we've verified the metadata that we
+  // need to do so.
+  iree_status_t verify_status = iree_ok_status();
+#if IREE_VM_BYTECODE_VERIFICATION_ENABLE
+  for (uint16_t i = 0; i < module->function_descriptor_count; ++i) {
+    IREE_TRACE_ZONE_BEGIN_NAMED(z1, "iree_vm_bytecode_function_verify");
+    verify_status = iree_vm_bytecode_function_verify(module, i, allocator);
+    IREE_TRACE_ZONE_END(z1);
+    if (!iree_status_is_ok(verify_status)) break;
+  }
+#endif  // IREE_VM_BYTECODE_VERIFICATION_ENABLE
+  if (iree_status_is_ok(verify_status)) {
+    *out_module = &module->interface;
+  } else {
+    iree_allocator_free(allocator, module);
+  }
+
   IREE_TRACE_ZONE_END(z0);
-  return iree_ok_status();
+  return verify_status;
 }
diff --git a/runtime/src/iree/vm/bytecode/module.h b/runtime/src/iree/vm/bytecode/module.h
index 93ebd82..997762f 100644
--- a/runtime/src/iree/vm/bytecode/module.h
+++ b/runtime/src/iree/vm/bytecode/module.h
@@ -7,8 +7,6 @@
 #ifndef IREE_VM_BYTECODE_MODULE_H_
 #define IREE_VM_BYTECODE_MODULE_H_
 
-#include <stdint.h>
-
 #include "iree/base/api.h"
 #include "iree/vm/api.h"
 
@@ -25,15 +23,6 @@
     iree_allocator_t archive_allocator, iree_allocator_t allocator,
     iree_vm_module_t** out_module);
 
-// Parses the module archive header in |archive_contents|.
-// The subrange containing the FlatBuffer data is returned as well as the
-// offset where external rodata begins. Note that archives may have
-// non-contiguous layouts!
-IREE_API_EXPORT iree_status_t iree_vm_bytecode_module_parse_header(
-    iree_const_byte_span_t archive_contents,
-    iree_const_byte_span_t* out_flatbuffer_contents,
-    iree_host_size_t* out_rodata_offset);
-
 #ifdef __cplusplus
 }  // extern "C"
 #endif  // __cplusplus
diff --git a/runtime/src/iree/vm/bytecode/module_benchmark.mlir b/runtime/src/iree/vm/bytecode/module_benchmark.mlir
index 10c7fd4..a2b8c85 100644
--- a/runtime/src/iree/vm/bytecode/module_benchmark.mlir
+++ b/runtime/src/iree/vm/bytecode/module_benchmark.mlir
@@ -86,9 +86,9 @@
     %c1 = vm.const.i64 1
     %c4 = vm.const.i64 4
     %count_i64 = vm.ext.i32.i64.u %count : i32 -> i64
-    %max = vm.mul.i64 %count_i64, %c4 : i64
-    %buf = vm.buffer.alloc %max : !vm.buffer
-    vm.buffer.fill.i32 %buf, %c0, %max, %pattern : i32 -> !vm.buffer
+    %count_bytes = vm.mul.i64 %count_i64, %c4 : i64
+    %buf = vm.buffer.alloc %count_bytes : !vm.buffer
+    vm.buffer.fill.i32 %buf, %c0, %count_i64, %pattern : i32 -> !vm.buffer
     vm.br ^loop(%c0, %c0_i32 : i64, i32)
   ^loop(%i : i64, %sum : i32):
     %element = vm.buffer.load.i32 %buf[%i] : !vm.buffer -> i32
@@ -109,9 +109,9 @@
     %c1 = vm.const.i64 1
     %c4 = vm.const.i64 4
     %count_i64 = vm.ext.i32.i64.u %count : i32 -> i64
-    %max = vm.mul.i64 %count_i64, %c4 : i64
-    %buf = vm.buffer.alloc %max : !vm.buffer
-    vm.buffer.fill.i32 %buf, %c0, %max, %pattern : i32 -> !vm.buffer
+    %count_bytes = vm.mul.i64 %count_i64, %c4 : i64
+    %buf = vm.buffer.alloc %count_bytes : !vm.buffer
+    vm.buffer.fill.i32 %buf, %c0, %count_i64, %pattern : i32 -> !vm.buffer
     %sum_init = vm.const.i32.zero
     vm.br ^loop(%c0, %sum_init : i64, i32)
   ^loop(%i0 : i64, %sum : i32):
diff --git a/runtime/src/iree/vm/bytecode/module_impl.h b/runtime/src/iree/vm/bytecode/module_impl.h
index 6090f94..70c2f3f 100644
--- a/runtime/src/iree/vm/bytecode/module_impl.h
+++ b/runtime/src/iree/vm/bytecode/module_impl.h
@@ -10,48 +10,14 @@
 #include <stdint.h>
 #include <string.h>
 
-// VC++ does not have C11's stdalign.h.
-#if !defined(_MSC_VER)
-#include <stdalign.h>
-#endif  // _MSC_VER
-
 #include "iree/base/api.h"
 #include "iree/vm/api.h"
-
-// NOTE: include order matters:
-#include "iree/base/internal/flatcc/parsing.h"
-#include "iree/schemas/bytecode_module_def_reader.h"
-#include "iree/schemas/bytecode_module_def_verifier.h"
+#include "iree/vm/bytecode/utils/isa.h"
 
 #ifdef __cplusplus
 extern "C" {
 #endif  // __cplusplus
 
-#define VMMAX(a, b) (((a) > (b)) ? (a) : (b))
-#define VMMIN(a, b) (((a) < (b)) ? (a) : (b))
-
-// Major bytecode version; mismatches on this will fail in either direction.
-// This allows coarse versioning of completely incompatible versions.
-// Matches BytecodeEncoder::kVersionMajor in the compiler.
-#define IREE_VM_BYTECODE_VERSION_MAJOR 13
-// Minor bytecode version; lower versions are allowed to enable newer runtimes
-// to load older serialized files when there are backwards-compatible changes.
-// Higher versions are disallowed as they occur when new ops are added that
-// otherwise cannot be executed by older runtimes.
-// Matches BytecodeEncoder::kVersionMinor in the compiler.
-#define IREE_VM_BYTECODE_VERSION_MINOR 0
-
-// Maximum register count per bank.
-// This determines the bits required to reference registers in the VM bytecode.
-#define IREE_I32_REGISTER_COUNT 0x7FFF
-#define IREE_REF_REGISTER_COUNT 0x7FFF
-
-#define IREE_I32_REGISTER_MASK 0x7FFF
-
-#define IREE_REF_REGISTER_TYPE_BIT 0x8000
-#define IREE_REF_REGISTER_MOVE_BIT 0x4000
-#define IREE_REF_REGISTER_MASK 0x3FFF
-
 // A loaded bytecode module.
 typedef struct iree_vm_bytecode_module_t {
   // Interface routing to the bytecode module functions.
diff --git a/runtime/src/iree/vm/bytecode/utils/BUILD b/runtime/src/iree/vm/bytecode/utils/BUILD
new file mode 100644
index 0000000..c8ec3be
--- /dev/null
+++ b/runtime/src/iree/vm/bytecode/utils/BUILD
@@ -0,0 +1,66 @@
+# Copyright 2023 The IREE Authors
+#
+# Licensed under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+load("//build_tools/bazel:build_defs.oss.bzl", "iree_runtime_cc_library", "iree_runtime_cc_test")
+
+package(
+    default_visibility = ["//visibility:public"],
+    features = ["layering_check"],
+    licenses = ["notice"],  # Apache 2.0
+)
+
+iree_runtime_cc_library(
+    name = "utils",
+    srcs = [
+        "block_list.c",
+        "features.c",
+    ],
+    hdrs = [
+        "block_list.h",
+        "features.h",
+        "generated/op_table.h",
+        "isa.h",
+    ],
+    deps = [
+        "//runtime/src/iree/base",
+        "//runtime/src/iree/base:tracing",
+        "//runtime/src/iree/base/internal",
+        "//runtime/src/iree/base/internal/flatcc:parsing",
+        "//runtime/src/iree/schemas:bytecode_module_def_c_fbs",
+        "//runtime/src/iree/vm",
+    ],
+)
+
+# TODO(#357): Add a script to update op_table.h.
+# iree_gentbl_cc_library(
+#     name = "op_table_gen",
+#     tbl_outs = [
+#         (["--gen-iree-vm-op-table-defs"], "op_table.h"),
+#     ],
+#     tblgen = "//tools:iree-tblgen",
+#     td_file = "//compiler/src/iree/compiler/Dialect/VM/IR:VMOps.td",
+#     deps = [
+#         "//compiler/src/iree/compiler/Dialect/Util/IR:td_files",
+#         "//compiler/src/iree/compiler/Dialect/VM/IR:td_files",
+#         "@llvm-project//mlir:CallInterfacesTdFiles",
+#         "@llvm-project//mlir:ControlFlowInterfacesTdFiles",
+#         "@llvm-project//mlir:FunctionInterfacesTdFiles",
+#         "@llvm-project//mlir:OpBaseTdFiles",
+#         "@llvm-project//mlir:SideEffectInterfacesTdFiles",
+#     ],
+# )
+
+iree_runtime_cc_test(
+    name = "block_list_test",
+    srcs = ["block_list_test.cc"],
+    deps = [
+        ":utils",
+        "//runtime/src/iree/base",
+        "//runtime/src/iree/testing:gtest",
+        "//runtime/src/iree/testing:gtest_main",
+        "//runtime/src/iree/vm",
+    ],
+)
diff --git a/runtime/src/iree/vm/bytecode/utils/CMakeLists.txt b/runtime/src/iree/vm/bytecode/utils/CMakeLists.txt
new file mode 100644
index 0000000..fa539dc
--- /dev/null
+++ b/runtime/src/iree/vm/bytecode/utils/CMakeLists.txt
@@ -0,0 +1,47 @@
+################################################################################
+# Autogenerated by build_tools/bazel_to_cmake/bazel_to_cmake.py from           #
+# runtime/src/iree/vm/bytecode/utils/BUILD                                     #
+#                                                                              #
+# Use iree_cmake_extra_content from iree/build_defs.oss.bzl to add arbitrary   #
+# CMake-only content.                                                          #
+#                                                                              #
+# To disable autogeneration for this file entirely, delete this header.        #
+################################################################################
+
+iree_add_all_subdirs()
+
+iree_cc_library(
+  NAME
+    utils
+  HDRS
+    "block_list.h"
+    "features.h"
+    "generated/op_table.h"
+    "isa.h"
+  SRCS
+    "block_list.c"
+    "features.c"
+  DEPS
+    iree::base
+    iree::base::internal
+    iree::base::internal::flatcc::parsing
+    iree::base::tracing
+    iree::schemas::bytecode_module_def_c_fbs
+    iree::vm
+  PUBLIC
+)
+
+iree_cc_test(
+  NAME
+    block_list_test
+  SRCS
+    "block_list_test.cc"
+  DEPS
+    ::utils
+    iree::base
+    iree::testing::gtest
+    iree::testing::gtest_main
+    iree::vm
+)
+
+### BAZEL_TO_CMAKE_PRESERVES_ALL_CONTENT_BELOW_THIS_LINE ###
diff --git a/runtime/src/iree/vm/bytecode/utils/block_list.c b/runtime/src/iree/vm/bytecode/utils/block_list.c
new file mode 100644
index 0000000..d892893
--- /dev/null
+++ b/runtime/src/iree/vm/bytecode/utils/block_list.c
@@ -0,0 +1,178 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "iree/vm/bytecode/utils/block_list.h"
+
+#include "iree/base/tracing.h"
+
+iree_status_t iree_vm_bytecode_block_list_initialize(
+    uint32_t capacity, iree_allocator_t allocator,
+    iree_vm_bytecode_block_list_t* out_block_list) {
+  IREE_TRACE_ZONE_BEGIN(z0);
+  IREE_ASSERT_ARGUMENT(out_block_list);
+
+  // In case we fail present an empty list.
+  out_block_list->capacity = 0;
+  out_block_list->count = 0;
+  out_block_list->values = NULL;
+
+  // Configure storage either inline if it fits or as a heap allocation.
+  if (capacity > IREE_ARRAYSIZE(out_block_list->inline_storage)) {
+    IREE_RETURN_AND_END_ZONE_IF_ERROR(
+        z0, iree_allocator_malloc(allocator,
+                                  sizeof(out_block_list->values[0]) * capacity,
+                                  (void**)&out_block_list->values));
+  } else {
+    out_block_list->values = out_block_list->inline_storage;
+  }
+
+  // Reset state and clear only the blocks we are using.
+  out_block_list->capacity = capacity;
+  out_block_list->count = 0;
+  memset(out_block_list->values, 0,
+         sizeof(out_block_list->values[0]) * capacity);
+
+  IREE_TRACE_ZONE_END(z0);
+  return iree_ok_status();
+}
+
+void iree_vm_bytecode_block_list_deinitialize(
+    iree_vm_bytecode_block_list_t* block_list, iree_allocator_t allocator) {
+  if (!block_list) return;
+  IREE_TRACE_ZONE_BEGIN(z0);
+
+  if (block_list->values != block_list->inline_storage) {
+    iree_allocator_free(allocator, block_list->values);
+  }
+  block_list->capacity = 0;
+  block_list->count = 0;
+  block_list->values = NULL;
+
+  IREE_TRACE_ZONE_END(z0);
+}
+
+iree_status_t iree_vm_bytecode_block_list_insert(
+    iree_vm_bytecode_block_list_t* block_list, uint32_t pc,
+    iree_vm_bytecode_block_t** out_block) {
+  IREE_ASSERT_ARGUMENT(block_list);
+  *out_block = NULL;
+
+  if (IREE_UNLIKELY(pc >= IREE_VM_PC_BLOCK_MAX)) {
+    return iree_make_status(IREE_STATUS_OUT_OF_RANGE,
+                            "block pc %08X greater than max %08X", pc,
+                            IREE_VM_PC_BLOCK_MAX);
+  }
+
+  // Try to find the block or the next block greater than it in the list in case
+  // we need to insert.
+  iree_host_size_t ordinal = 0;
+  iree_status_t status =
+      iree_vm_bytecode_block_list_find(block_list, pc, &ordinal);
+  if (iree_status_is_ok(status)) {
+    // Block found.
+    *out_block = &block_list->values[ordinal];
+    return iree_ok_status();
+  }
+  status = iree_status_ignore(status);
+
+  // Not found, need to insert at ordinal point at the next greatest pc.
+  if (IREE_UNLIKELY(block_list->count + 1 > block_list->capacity)) {
+    return iree_make_status(
+        IREE_STATUS_INVALID_ARGUMENT,
+        "invalid descriptor block count %u; found at least %u blocks",
+        block_list->capacity, block_list->count + 1);
+  }
+
+  // Shift list up and declare new block.
+  if (ordinal != block_list->count) {
+    memmove(&block_list->values[ordinal + 1], &block_list->values[ordinal],
+            (block_list->count - ordinal) * sizeof(block_list->values[0]));
+  }
+  iree_vm_bytecode_block_t* block = &block_list->values[ordinal];
+  block->defined = 0;
+  block->reserved = 0;
+  block->pc = pc;
+
+  ++block_list->count;
+  *out_block = block;
+  return iree_ok_status();
+}
+
+// Finds the ordinal of the block with the given |pc| within the block list.
+// Note that these ordinals will change with each insertion and this is
+// generally only safe to use after the list has been completed.
+// If NOT_FOUND then |out_ordinal| will contain the index into the list of where
+// the block would be inserted.
+iree_status_t iree_vm_bytecode_block_list_find(
+    const iree_vm_bytecode_block_list_t* block_list, uint32_t pc,
+    iree_host_size_t* out_ordinal) {
+  IREE_ASSERT_ARGUMENT(block_list);
+  *out_ordinal = 0;
+  int low = 0;
+  int high = (int)block_list->count - 1;
+  while (low <= high) {
+    const int mid = low + (high - low) / 2;
+    const uint32_t mid_pc = block_list->values[mid].pc;
+    if (mid_pc < pc) {
+      low = mid + 1;
+    } else if (mid_pc > pc) {
+      high = mid - 1;
+    } else {
+      // Found; early exit.
+      *out_ordinal = mid;
+      return iree_ok_status();
+    }
+  }
+  // Not found; return the next highest slot. Note that this may be off the
+  // end of the list if the search pc is greater than all current values.
+  *out_ordinal = low;
+  return iree_status_from_code(IREE_STATUS_NOT_FOUND);
+}
+
+iree_status_t iree_vm_bytecode_block_list_verify(
+    const iree_vm_bytecode_block_list_t* block_list,
+    iree_const_byte_span_t bytecode_data) {
+  IREE_ASSERT_ARGUMENT(block_list);
+
+  // Ensure we have as many blocks as expected.
+  if (block_list->count != block_list->capacity) {
+    return iree_make_status(
+        IREE_STATUS_INVALID_ARGUMENT,
+        "invalid descriptor block count %u; found %u blocks",
+        block_list->capacity, block_list->count);
+  }
+
+  for (uint32_t i = 0; i < block_list->count; ++i) {
+    const iree_vm_bytecode_block_t* block = &block_list->values[i];
+
+    // Ensure all blocks are defined.
+    if (!block->defined) {
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "block at pc %08X not defined in bytecode",
+                              block->pc);
+    }
+
+    // Ensure each block pc is in bounds - we need at least 1 byte for the
+    // marker.
+    if (block->pc + 1 >= bytecode_data.data_length) {
+      return iree_make_status(
+          IREE_STATUS_INVALID_ARGUMENT,
+          "block at pc %08X (%u) out of bytecode data range %" PRIhsz,
+          block->pc, block->pc, bytecode_data.data_length);
+    }
+
+    // Ensure each block has a block opcode at its target.
+    uint8_t opc = bytecode_data.data[block->pc];
+    if (opc != IREE_VM_OP_CORE_Block) {
+      return iree_make_status(
+          IREE_STATUS_INVALID_ARGUMENT,
+          "block at pc %08X does not start with a block marker opcode",
+          block->pc);
+    }
+  }
+
+  return iree_ok_status();
+}
diff --git a/runtime/src/iree/vm/bytecode/utils/block_list.h b/runtime/src/iree/vm/bytecode/utils/block_list.h
new file mode 100644
index 0000000..65e6960
--- /dev/null
+++ b/runtime/src/iree/vm/bytecode/utils/block_list.h
@@ -0,0 +1,90 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef IREE_VM_BYTECODE_UTILS_BLOCK_LIST_H_
+#define IREE_VM_BYTECODE_UTILS_BLOCK_LIST_H_
+
+#include "iree/base/api.h"
+#include "iree/vm/api.h"
+#include "iree/vm/bytecode/utils/isa.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif  // __cplusplus
+
+// Reserved inline storage capacity for blocks in the block list.
+// If more blocks than this are requested we'll heap allocate instead.
+// Most programs today end up with a small number of blocks (sometimes 2-3 for
+// the entire program).
+#define IREE_VM_BYTECODE_INLINE_BLOCK_LIST_CAPACITY (32)
+
+// A tracked block within the block list.
+// Blocks may be either declared (observed as a branch target) or defined
+// (observed within the bytecode stream).
+typedef struct iree_vm_bytecode_block_t {
+  // Set only if the block definition has been seen.
+  uint32_t defined : 1;
+  uint32_t reserved : 7;
+  // Program counter of the block within the function.
+  uint32_t pc : 24;
+} iree_vm_bytecode_block_t;
+
+// A sorted list of blocks.
+// Allows for single-pass verification
+typedef struct iree_vm_bytecode_block_list_t {
+  // Available capacity in |values| and the total expected block count.
+  uint32_t capacity;
+  // Current count of blocks in the |values| list.
+  uint32_t count;
+  // List of blocks sorted by program counter.
+  // Will either point to inline_storage or be a heap allocation.
+  iree_vm_bytecode_block_t* values;
+  // Inlined storage for reasonable block counts to avoid the heap alloc.
+  iree_vm_bytecode_block_t
+      inline_storage[IREE_VM_BYTECODE_INLINE_BLOCK_LIST_CAPACITY];
+} iree_vm_bytecode_block_list_t;
+
+// Initializes a block list with the expected count of |capacity|.
+// The same |allocator| must be passed to
+// iree_vm_bytecode_block_list_deinitialize; the expectation is that the hosting
+// data structure already has a reference to the allocator.
+iree_status_t iree_vm_bytecode_block_list_initialize(
+    uint32_t capacity, iree_allocator_t allocator,
+    iree_vm_bytecode_block_list_t* out_block_list);
+
+// Deinitializes a block list using |allocator| if any heap allocations were
+// required (must be the same as passed to
+// iree_vm_bytecode_block_list_initialize).
+void iree_vm_bytecode_block_list_deinitialize(
+    iree_vm_bytecode_block_list_t* block_list, iree_allocator_t allocator);
+
+// Looks up a block at |pc| in the |block_list|. If not found the block is
+// inserted as declared. Returns the block. Fails if capacity is exceeded.
+// The returned |out_block| pointer is only valid until the next insertion.
+iree_status_t iree_vm_bytecode_block_list_insert(
+    iree_vm_bytecode_block_list_t* block_list, uint32_t pc,
+    iree_vm_bytecode_block_t** out_block);
+
+// Finds the ordinal of the block with the given |pc| within the block list.
+// Note that these ordinals will change with each insertion and this is
+// generally only safe to use after the list has been completed.
+// If NOT_FOUND then |out_ordinal| will contain the index into the list of where
+// the block would be inserted.
+iree_status_t iree_vm_bytecode_block_list_find(
+    const iree_vm_bytecode_block_list_t* block_list, uint32_t pc,
+    iree_host_size_t* out_ordinal);
+
+// Verifies that all blocks in the block list were defined and have proper
+// tracking in |bytecode_data|.
+iree_status_t iree_vm_bytecode_block_list_verify(
+    const iree_vm_bytecode_block_list_t* block_list,
+    iree_const_byte_span_t bytecode_data);
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif  // __cplusplus
+
+#endif  // IREE_VM_BYTECODE_UTILS_BLOCK_LIST_H_
diff --git a/runtime/src/iree/vm/bytecode/utils/block_list_test.cc b/runtime/src/iree/vm/bytecode/utils/block_list_test.cc
new file mode 100644
index 0000000..bba61c8
--- /dev/null
+++ b/runtime/src/iree/vm/bytecode/utils/block_list_test.cc
@@ -0,0 +1,313 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "iree/vm/bytecode/utils/block_list.h"
+
+#include "iree/base/api.h"
+#include "iree/testing/gtest.h"
+#include "iree/testing/status_matchers.h"
+
+namespace {
+
+using iree::Status;
+using iree::StatusCode;
+using iree::testing::status::IsOkAndHolds;
+using iree::testing::status::StatusIs;
+using testing::ElementsAre;
+using testing::Eq;
+
+// Tests usage on empty lists.
+TEST(BlockListTest, Empty) {
+  iree_allocator_t allocator = iree_allocator_system();
+  iree_vm_bytecode_block_list_t block_list;
+  IREE_ASSERT_OK(
+      iree_vm_bytecode_block_list_initialize(0u, allocator, &block_list));
+
+  // Try finding with an empty list. Note that we expect the ordinal to be valid
+  // even though we can't insert anything.
+  iree_host_size_t ordinal = 0;
+  EXPECT_THAT(
+      Status(iree_vm_bytecode_block_list_find(&block_list, 0u, &ordinal)),
+      StatusIs(StatusCode::kNotFound));
+  EXPECT_EQ(ordinal, 0);
+  EXPECT_THAT(
+      Status(iree_vm_bytecode_block_list_find(&block_list, 123u, &ordinal)),
+      StatusIs(StatusCode::kNotFound));
+  EXPECT_EQ(ordinal, 0);
+
+  // No blocks inserted to verify.
+  IREE_EXPECT_OK(iree_vm_bytecode_block_list_verify(
+      &block_list, iree_const_byte_span_empty()));
+
+  iree_vm_bytecode_block_list_deinitialize(&block_list, allocator);
+}
+
+// Valid IR usage for 3 blocks. Note that we insert them out of order: 1 2 0.
+// These should be stored inline in the block list struct.
+TEST(BlockListTest, Valid) {
+  iree_allocator_t allocator = iree_allocator_system();
+  iree_vm_bytecode_block_list_t block_list;
+  IREE_ASSERT_OK(
+      iree_vm_bytecode_block_list_initialize(3u, allocator, &block_list));
+
+  // Try finding blocks before anything is defined.
+  iree_host_size_t ordinal = 0;
+  EXPECT_THAT(
+      Status(iree_vm_bytecode_block_list_find(&block_list, 0u, &ordinal)),
+      StatusIs(StatusCode::kNotFound));
+  EXPECT_EQ(ordinal, 0);
+  EXPECT_THAT(
+      Status(iree_vm_bytecode_block_list_find(&block_list, 123u, &ordinal)),
+      StatusIs(StatusCode::kNotFound));
+  EXPECT_EQ(ordinal, 0);
+
+  iree_vm_bytecode_block_t* block = NULL;
+
+  // Define block 1.
+  block = NULL;
+  IREE_ASSERT_OK(iree_vm_bytecode_block_list_insert(&block_list, 1u, &block));
+  EXPECT_EQ(block_list.count, 1);
+  EXPECT_EQ(block->defined, 0);
+  EXPECT_EQ(block->pc, 1u);
+  block->defined = 1;
+
+  // Define block 2.
+  block = NULL;
+  IREE_ASSERT_OK(iree_vm_bytecode_block_list_insert(&block_list, 2u, &block));
+  EXPECT_EQ(block_list.count, 2);
+  EXPECT_EQ(block->defined, 0);
+  EXPECT_EQ(block->pc, 2u);
+  block->defined = 1;
+
+  // Define block 0.
+  block = NULL;
+  IREE_ASSERT_OK(iree_vm_bytecode_block_list_insert(&block_list, 0u, &block));
+  EXPECT_EQ(block_list.count, 3);
+  EXPECT_EQ(block->defined, 0);
+  EXPECT_EQ(block->pc, 0u);
+  block->defined = 1;
+
+  // Re-insert block 1. Should be a no-op as it is defined.
+  block = NULL;
+  IREE_ASSERT_OK(iree_vm_bytecode_block_list_insert(&block_list, 1u, &block));
+  EXPECT_EQ(block_list.count, 3);
+  EXPECT_EQ(block->defined, 1);
+  EXPECT_EQ(block->pc, 1u);
+
+  // Find each block and ensure they match.
+  IREE_EXPECT_OK(iree_vm_bytecode_block_list_find(&block_list, 0u, &ordinal));
+  EXPECT_EQ(ordinal, 0);
+  IREE_EXPECT_OK(iree_vm_bytecode_block_list_find(&block_list, 1u, &ordinal));
+  EXPECT_EQ(ordinal, 1);
+  IREE_EXPECT_OK(iree_vm_bytecode_block_list_find(&block_list, 2u, &ordinal));
+  EXPECT_EQ(ordinal, 2);
+
+  // Verify blocks are all defined and have block markers.
+  std::vector<uint8_t> bytecode_data = {
+      IREE_VM_OP_CORE_Block, IREE_VM_OP_CORE_Block, IREE_VM_OP_CORE_Block,
+      IREE_VM_OP_CORE_AbsI32,  // need at least one op in a block
+  };
+  IREE_EXPECT_OK(iree_vm_bytecode_block_list_verify(
+      &block_list,
+      iree_make_const_byte_span(bytecode_data.data(), bytecode_data.size())));
+
+  iree_vm_bytecode_block_list_deinitialize(&block_list, allocator);
+}
+
+// Tests that a declared block that was never defined errors on verification.
+TEST(BlockListTest, Undefined) {
+  iree_allocator_t allocator = iree_allocator_system();
+  iree_vm_bytecode_block_list_t block_list;
+  IREE_ASSERT_OK(
+      iree_vm_bytecode_block_list_initialize(1u, allocator, &block_list));
+
+  // Declare the block.
+  iree_vm_bytecode_block_t* block = NULL;
+  IREE_ASSERT_OK(iree_vm_bytecode_block_list_insert(&block_list, 0u, &block));
+
+  // Fail verification because it hasn't been defined.
+  std::vector<uint8_t> bytecode_data = {
+      IREE_VM_OP_CORE_Block,
+  };
+  EXPECT_THAT(
+      Status(iree_vm_bytecode_block_list_verify(
+          &block_list, iree_make_const_byte_span(bytecode_data.data(),
+                                                 bytecode_data.size()))),
+      StatusIs(StatusCode::kInvalidArgument));
+
+  iree_vm_bytecode_block_list_deinitialize(&block_list, allocator);
+}
+
+// Tests adding fewer blocks than expected by the capacity.
+TEST(BlockListTest, Underflow) {
+  iree_allocator_t allocator = iree_allocator_system();
+  iree_vm_bytecode_block_list_t block_list;
+  IREE_ASSERT_OK(
+      iree_vm_bytecode_block_list_initialize(2u, allocator, &block_list));
+
+  // Declaring; OK (count = 1, capacity = 2).
+  iree_vm_bytecode_block_t* block = NULL;
+  IREE_ASSERT_OK(iree_vm_bytecode_block_list_insert(&block_list, 0u, &block));
+
+  // Defining; OK (no change in count).
+  IREE_ASSERT_OK(iree_vm_bytecode_block_list_insert(&block_list, 0u, &block));
+  block->defined = 1;
+
+  // Fail verification because we're missing a block.
+  std::vector<uint8_t> bytecode_data = {
+      IREE_VM_OP_CORE_Block,
+      IREE_VM_OP_CORE_AbsI32,  // need at least one op in a block
+  };
+  EXPECT_THAT(
+      Status(iree_vm_bytecode_block_list_verify(
+          &block_list, iree_make_const_byte_span(bytecode_data.data(),
+                                                 bytecode_data.size()))),
+      StatusIs(StatusCode::kInvalidArgument));
+
+  iree_vm_bytecode_block_list_deinitialize(&block_list, allocator);
+}
+
+// Tests adding more blocks than allowed by the capacity.
+TEST(BlockListTest, Overflow) {
+  iree_allocator_t allocator = iree_allocator_system();
+  iree_vm_bytecode_block_list_t block_list;
+  IREE_ASSERT_OK(
+      iree_vm_bytecode_block_list_initialize(1u, allocator, &block_list));
+
+  // Declaring; OK (count = 1, capacity = 1).
+  iree_vm_bytecode_block_t* block = NULL;
+  IREE_ASSERT_OK(iree_vm_bytecode_block_list_insert(&block_list, 0u, &block));
+  EXPECT_EQ(block_list.count, 1);
+
+  // Defining; OK (no change in count).
+  IREE_ASSERT_OK(iree_vm_bytecode_block_list_insert(&block_list, 0u, &block));
+  block->defined = 1;
+
+  // Error: too many blocks.
+  EXPECT_THAT(
+      Status(iree_vm_bytecode_block_list_insert(&block_list, 1u, &block)),
+      StatusIs(StatusCode::kInvalidArgument));
+  EXPECT_EQ(block_list.count, 1);
+
+  iree_vm_bytecode_block_list_deinitialize(&block_list, allocator);
+}
+
+// Tests adding any blocks to an expected-empty list.
+TEST(BlockListTest, OverflowEmpty) {
+  iree_allocator_t allocator = iree_allocator_system();
+  iree_vm_bytecode_block_list_t block_list;
+  IREE_ASSERT_OK(
+      iree_vm_bytecode_block_list_initialize(0u, allocator, &block_list));
+
+  // Error: too many blocks.
+  iree_vm_bytecode_block_t* block = NULL;
+  EXPECT_THAT(
+      Status(iree_vm_bytecode_block_list_insert(&block_list, 0u, &block)),
+      StatusIs(StatusCode::kInvalidArgument));
+  EXPECT_EQ(block_list.count, 0);
+
+  iree_vm_bytecode_block_list_deinitialize(&block_list, allocator);
+}
+
+// Tests a block that is missing its marker in the bytecode.
+TEST(BlockListTest, MissingMarker) {
+  iree_allocator_t allocator = iree_allocator_system();
+  iree_vm_bytecode_block_list_t block_list;
+  IREE_ASSERT_OK(
+      iree_vm_bytecode_block_list_initialize(1u, allocator, &block_list));
+
+  iree_vm_bytecode_block_t* block = NULL;
+  IREE_ASSERT_OK(iree_vm_bytecode_block_list_insert(&block_list, 0u, &block));
+  block->defined = 1;
+
+  std::vector<uint8_t> bytecode_data = {
+      IREE_VM_OP_CORE_AbsI32,  // *not* the block marker
+  };
+  EXPECT_THAT(
+      Status(iree_vm_bytecode_block_list_verify(
+          &block_list, iree_make_const_byte_span(bytecode_data.data(),
+                                                 bytecode_data.size()))),
+      StatusIs(StatusCode::kInvalidArgument));
+
+  iree_vm_bytecode_block_list_deinitialize(&block_list, allocator);
+}
+
+// Tests a block with a pc outside of the bytecode range.
+TEST(BlockListTest, OutOfBoundsPC) {
+  iree_allocator_t allocator = iree_allocator_system();
+  iree_vm_bytecode_block_list_t block_list;
+  IREE_ASSERT_OK(
+      iree_vm_bytecode_block_list_initialize(1u, allocator, &block_list));
+
+  iree_vm_bytecode_block_t* block = NULL;
+  IREE_ASSERT_OK(iree_vm_bytecode_block_list_insert(&block_list, 1u, &block));
+  block->defined = 1;
+
+  std::vector<uint8_t> bytecode_data = {
+      IREE_VM_OP_CORE_AbsI32,  // *not* the block marker
+  };
+  EXPECT_THAT(
+      Status(iree_vm_bytecode_block_list_verify(
+          &block_list, iree_make_const_byte_span(bytecode_data.data(),
+                                                 bytecode_data.size()))),
+      StatusIs(StatusCode::kInvalidArgument));
+
+  iree_vm_bytecode_block_list_deinitialize(&block_list, allocator);
+}
+
+// Tests inserting a block with a PC outside of what we can track. This should
+// be really rare in practice.
+TEST(BlockListTest, OverMaxPC) {
+  iree_allocator_t allocator = iree_allocator_system();
+  iree_vm_bytecode_block_list_t block_list;
+  IREE_ASSERT_OK(
+      iree_vm_bytecode_block_list_initialize(1u, allocator, &block_list));
+
+  iree_vm_bytecode_block_t* block = NULL;
+  EXPECT_THAT(Status(iree_vm_bytecode_block_list_insert(
+                  &block_list, IREE_VM_PC_BLOCK_MAX + 1, &block)),
+              StatusIs(StatusCode::kOutOfRange));
+
+  iree_vm_bytecode_block_list_deinitialize(&block_list, allocator);
+}
+
+// Tests adding a lot of blocks such that we trigger a heap storage allocation.
+TEST(BlockListTest, HeapStorage) {
+  uint32_t count = IREE_VM_BYTECODE_INLINE_BLOCK_LIST_CAPACITY * 8;
+  iree_allocator_t allocator = iree_allocator_system();
+  iree_vm_bytecode_block_list_t block_list;
+  IREE_ASSERT_OK(
+      iree_vm_bytecode_block_list_initialize(count, allocator, &block_list));
+
+  // Declare all blocks in reverse, for fun.
+  for (uint32_t i = 0; i < count; ++i) {
+    iree_vm_bytecode_block_t* block = NULL;
+    IREE_ASSERT_OK(
+        iree_vm_bytecode_block_list_insert(&block_list, count - i - 1, &block));
+  }
+
+  // Ensure sorted.
+  for (uint32_t i = 0; i < count; ++i) {
+    EXPECT_EQ(block_list.values[i].pc, i);
+  }
+
+  // Define all blocks forward.
+  for (uint32_t i = 0; i < count; ++i) {
+    iree_vm_bytecode_block_t* block = NULL;
+    IREE_ASSERT_OK(iree_vm_bytecode_block_list_insert(&block_list, i, &block));
+    block->defined = 1;
+  }
+
+  // Fake block data (+1 trailing block op for padding) to verify.
+  std::vector<uint8_t> bytecode_data(count + 1, IREE_VM_OP_CORE_Block);
+  IREE_EXPECT_OK(iree_vm_bytecode_block_list_verify(
+      &block_list,
+      iree_make_const_byte_span(bytecode_data.data(), bytecode_data.size())));
+
+  iree_vm_bytecode_block_list_deinitialize(&block_list, allocator);
+}
+
+}  // namespace
diff --git a/runtime/src/iree/vm/bytecode/utils/features.c b/runtime/src/iree/vm/bytecode/utils/features.c
new file mode 100644
index 0000000..6a333f9
--- /dev/null
+++ b/runtime/src/iree/vm/bytecode/utils/features.c
@@ -0,0 +1,60 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "iree/vm/bytecode/utils/features.h"
+
+// clang-format off
+static const iree_bitfield_string_mapping_t iree_vm_bytecode_feature_mappings[] = {
+  {iree_vm_FeatureBits_EXT_F32, IREE_SVL("EXT_F32")},
+  {iree_vm_FeatureBits_EXT_F64, IREE_SVL("EXT_F64")},
+};
+// clang-format on
+
+iree_string_view_t iree_vm_bytecode_features_format(
+    iree_vm_FeatureBits_enum_t value, iree_bitfield_string_temp_t* out_temp) {
+  return iree_bitfield_format_inline(
+      value, IREE_ARRAYSIZE(iree_vm_bytecode_feature_mappings),
+      iree_vm_bytecode_feature_mappings, out_temp);
+}
+
+iree_vm_FeatureBits_enum_t iree_vm_bytecode_available_features(void) {
+  iree_vm_FeatureBits_enum_t result = 0;
+#if IREE_VM_EXT_F32_ENABLE
+  result |= iree_vm_FeatureBits_EXT_F32;
+#endif  // IREE_VM_EXT_F32_ENABLE
+#if IREE_VM_EXT_F64_ENABLE
+  result |= iree_vm_FeatureBits_EXT_F64;
+#endif  // IREE_VM_EXT_F64_ENABLE
+  return result;
+}
+
+iree_status_t iree_vm_check_feature_mismatch(
+    const char* file, int line, iree_vm_FeatureBits_enum_t required_features,
+    iree_vm_FeatureBits_enum_t available_features) {
+  if (IREE_LIKELY(iree_all_bits_set(available_features, required_features))) {
+    return iree_ok_status();
+  }
+#if IREE_STATUS_MODE
+  const iree_vm_FeatureBits_enum_t needed_features =
+      required_features & ~available_features;
+  iree_bitfield_string_temp_t temp0, temp1, temp2;
+  iree_string_view_t available_features_str =
+      iree_vm_bytecode_features_format(available_features, &temp0);
+  iree_string_view_t required_features_str =
+      iree_vm_bytecode_features_format(required_features, &temp1);
+  iree_string_view_t needed_features_str =
+      iree_vm_bytecode_features_format(needed_features, &temp2);
+  return iree_make_status_with_location(
+      file, line, IREE_STATUS_INVALID_ARGUMENT,
+      "required module features [%.*s] are not available in this runtime "
+      "configuration; have [%.*s] while module requires [%.*s]",
+      (int)needed_features_str.size, needed_features_str.data,
+      (int)available_features_str.size, available_features_str.data,
+      (int)required_features_str.size, required_features_str.data);
+#else
+  return iree_status_from_code(IREE_STATUS_INVALID_ARGUMENT);
+#endif  // IREE_STATUS_MODE
+}
diff --git a/runtime/src/iree/vm/bytecode/utils/features.h b/runtime/src/iree/vm/bytecode/utils/features.h
new file mode 100644
index 0000000..6545803
--- /dev/null
+++ b/runtime/src/iree/vm/bytecode/utils/features.h
@@ -0,0 +1,37 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef IREE_VM_BYTECODE_UTILS_FEATURES_H_
+#define IREE_VM_BYTECODE_UTILS_FEATURES_H_
+
+#include "iree/base/api.h"
+#include "iree/vm/api.h"
+#include "iree/vm/bytecode/utils/isa.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif  // __cplusplus
+
+// Formats a buffer usage bitfield as a string.
+// See iree_bitfield_format for usage.
+iree_string_view_t iree_vm_bytecode_features_format(
+    iree_vm_FeatureBits_enum_t value, iree_bitfield_string_temp_t* out_temp);
+
+// Returns the features available in this build of the runtime.
+iree_vm_FeatureBits_enum_t iree_vm_bytecode_available_features(void);
+
+// Returns a pretty status reported at |file|/|line| when one or more features
+// from |required_features| is missing from |available_features|.
+// Returns OK if all required features are available.
+iree_status_t iree_vm_check_feature_mismatch(
+    const char* file, int line, iree_vm_FeatureBits_enum_t required_features,
+    iree_vm_FeatureBits_enum_t available_features);
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif  // __cplusplus
+
+#endif  // IREE_VM_BYTECODE_UTILS_FEATURES_H_
diff --git a/runtime/src/iree/vm/bytecode/generated/.clang-format b/runtime/src/iree/vm/bytecode/utils/generated/.clang-format
similarity index 100%
rename from runtime/src/iree/vm/bytecode/generated/.clang-format
rename to runtime/src/iree/vm/bytecode/utils/generated/.clang-format
diff --git a/runtime/src/iree/vm/bytecode/generated/op_table.h b/runtime/src/iree/vm/bytecode/utils/generated/op_table.h
similarity index 99%
rename from runtime/src/iree/vm/bytecode/generated/op_table.h
rename to runtime/src/iree/vm/bytecode/utils/generated/op_table.h
index b1f3a0b..3ad3c5b 100644
--- a/runtime/src/iree/vm/bytecode/generated/op_table.h
+++ b/runtime/src/iree/vm/bytecode/utils/generated/op_table.h
@@ -128,7 +128,7 @@
   IREE_VM_OP_CORE_CtlzI64 = 0x76,
   IREE_VM_OP_CORE_AbsI32 = 0x77,
   IREE_VM_OP_CORE_AbsI64 = 0x78,
-  IREE_VM_OP_CORE_RSV_0x79,
+  IREE_VM_OP_CORE_Block = 0x79,
   IREE_VM_OP_CORE_RSV_0x7A,
   IREE_VM_OP_CORE_RSV_0x7B,
   IREE_VM_OP_CORE_RSV_0x7C,
@@ -387,7 +387,7 @@
     OPC(0x76, CtlzI64) \
     OPC(0x77, AbsI32) \
     OPC(0x78, AbsI64) \
-    RSV(0x79) \
+    OPC(0x79, Block) \
     RSV(0x7A) \
     RSV(0x7B) \
     RSV(0x7C) \
diff --git a/runtime/src/iree/vm/bytecode/utils/isa.h b/runtime/src/iree/vm/bytecode/utils/isa.h
new file mode 100644
index 0000000..d24300c
--- /dev/null
+++ b/runtime/src/iree/vm/bytecode/utils/isa.h
@@ -0,0 +1,103 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef IREE_VM_BYTECODE_UTILS_ISA_H_
+#define IREE_VM_BYTECODE_UTILS_ISA_H_
+
+#include "iree/base/api.h"
+#include "iree/vm/api.h"
+#include "iree/vm/bytecode/utils/generated/op_table.h"  // IWYU pragma: export
+
+// NOTE: include order matters:
+#include "iree/base/internal/flatcc/parsing.h"          // IWYU pragma: export
+#include "iree/schemas/bytecode_module_def_reader.h"    // IWYU pragma: export
+#include "iree/schemas/bytecode_module_def_verifier.h"  // IWYU pragma: export
+
+//===----------------------------------------------------------------------===//
+// Misc utilities
+//===----------------------------------------------------------------------===//
+
+#define VMMAX(a, b) (((a) > (b)) ? (a) : (b))
+#define VMMIN(a, b) (((a) < (b)) ? (a) : (b))
+
+#define VM_AlignPC(pc, alignment) \
+  (pc) = ((pc) + ((alignment)-1)) & ~((alignment)-1)
+
+//===----------------------------------------------------------------------===//
+// Bytecode versioning
+//===----------------------------------------------------------------------===//
+
+// Major bytecode version; mismatches on this will fail in either direction.
+// This allows coarse versioning of completely incompatible versions.
+// Matches BytecodeEncoder::kVersionMajor in the compiler.
+#define IREE_VM_BYTECODE_VERSION_MAJOR 14
+// Minor bytecode version; lower versions are allowed to enable newer runtimes
+// to load older serialized files when there are backwards-compatible changes.
+// Higher versions are disallowed as they occur when new ops are added that
+// otherwise cannot be executed by older runtimes.
+// Matches BytecodeEncoder::kVersionMinor in the compiler.
+#define IREE_VM_BYTECODE_VERSION_MINOR 0
+
+//===----------------------------------------------------------------------===//
+// Bytecode structural constants
+//===----------------------------------------------------------------------===//
+
+// Size of a register ordinal in the bytecode.
+#define IREE_REGISTER_ORDINAL_SIZE sizeof(uint16_t)
+
+// Maximum register count per bank.
+// This determines the bits required to reference registers in the VM bytecode.
+#define IREE_I32_REGISTER_COUNT 0x7FFF
+#define IREE_REF_REGISTER_COUNT 0x3FFF
+
+#define IREE_I32_REGISTER_MASK 0x7FFF
+
+#define IREE_REF_REGISTER_TYPE_BIT 0x8000
+#define IREE_REF_REGISTER_MOVE_BIT 0x4000
+#define IREE_REF_REGISTER_MASK 0x3FFF
+
+// Maximum program counter offset within in a single block.
+// This is just so that we can steal bits for flags and such. 16MB (today)
+// should be more than enough for a single basic block. If not then we should
+// compress better!
+#define IREE_VM_PC_BLOCK_MAX 0x00FFFFFFu
+
+// Bytecode data -offset used when looking for the start of the currently
+// dispatched instruction: `instruction_start = pc - OFFSET`
+#define IREE_VM_PC_OFFSET_CORE 1
+#define IREE_VM_PC_OFFSET_EXT_I32 2
+#define IREE_VM_PC_OFFSET_EXT_F32 2
+#define IREE_VM_PC_OFFSET_EXT_F64 2
+
+// Interleaved src-dst register sets for branch register remapping.
+// This structure is an overlay for the bytecode that is serialized in a
+// matching format.
+typedef struct iree_vm_register_remap_list_t {
+  uint16_t size;
+  struct pair {
+    uint16_t src_reg;
+    uint16_t dst_reg;
+  } pairs[];
+} iree_vm_register_remap_list_t;
+static_assert(iree_alignof(iree_vm_register_remap_list_t) == 2,
+              "Expecting byte alignment (to avoid padding)");
+static_assert(offsetof(iree_vm_register_remap_list_t, pairs) == 2,
+              "Expect no padding in the struct");
+
+//===----------------------------------------------------------------------===//
+// Bytecode data reading with little-/big-endian support
+//===----------------------------------------------------------------------===//
+
+// Bytecode data access macros for reading values of a given type from a byte
+// offset within the current function.
+#define OP_I8(i) iree_unaligned_load_le((uint8_t*)&bytecode_data[pc + (i)])
+#define OP_I16(i) iree_unaligned_load_le((uint16_t*)&bytecode_data[pc + (i)])
+#define OP_I32(i) iree_unaligned_load_le((uint32_t*)&bytecode_data[pc + (i)])
+#define OP_I64(i) iree_unaligned_load_le((uint64_t*)&bytecode_data[pc + (i)])
+#define OP_F32(i) iree_unaligned_load_le((float*)&bytecode_data[pc + (i)])
+#define OP_F64(i) iree_unaligned_load_le((double*)&bytecode_data[pc + (i)])
+
+#endif  // IREE_VM_BYTECODE_UTILS_ISA_H_
diff --git a/runtime/src/iree/vm/bytecode/verifier.c b/runtime/src/iree/vm/bytecode/verifier.c
new file mode 100644
index 0000000..f26c4f3
--- /dev/null
+++ b/runtime/src/iree/vm/bytecode/verifier.c
@@ -0,0 +1,1863 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "iree/vm/bytecode/verifier.h"
+
+#include "iree/base/internal/math.h"
+#include "iree/vm/bytecode/utils/block_list.h"
+#include "iree/vm/bytecode/utils/features.h"
+
+//===----------------------------------------------------------------------===//
+// Module metadata verification
+//===----------------------------------------------------------------------===//
+
+iree_status_t iree_vm_bytecode_module_flatbuffer_verify(
+    iree_const_byte_span_t archive_contents,
+    iree_const_byte_span_t flatbuffer_contents,
+    iree_host_size_t archive_rodata_offset) {
+  // Run flatcc generated verification. This ensures all pointers are in-bounds
+  // and that we can safely walk the file, but not that the actual contents of
+  // the FlatBuffer meet our expectations.
+  int verify_ret = iree_vm_BytecodeModuleDef_verify_as_root(
+      flatbuffer_contents.data, flatbuffer_contents.data_length);
+  if (verify_ret != flatcc_verify_ok) {
+    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                            "FlatBuffer verification failed: %s",
+                            flatcc_verify_error_string(verify_ret));
+  }
+
+  iree_vm_BytecodeModuleDef_table_t module_def =
+      iree_vm_BytecodeModuleDef_as_root(flatbuffer_contents.data);
+
+  const iree_vm_FeatureBits_enum_t available_features =
+      iree_vm_bytecode_available_features();
+  const iree_vm_FeatureBits_enum_t required_features =
+      iree_vm_BytecodeModuleDef_requirements(module_def);
+  IREE_RETURN_IF_ERROR(iree_vm_check_feature_mismatch(
+      __FILE__, __LINE__, required_features, available_features));
+
+  flatbuffers_string_t name = iree_vm_BytecodeModuleDef_name(module_def);
+  if (!flatbuffers_string_len(name)) {
+    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                            "module missing name field");
+  }
+
+  iree_vm_TypeDef_vec_t types = iree_vm_BytecodeModuleDef_types(module_def);
+  for (size_t i = 0; i < iree_vm_TypeDef_vec_len(types); ++i) {
+    iree_vm_TypeDef_table_t type_def = iree_vm_TypeDef_vec_at(types, i);
+    if (!type_def) {
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "types[%zu] missing body", i);
+    }
+    flatbuffers_string_t full_name = iree_vm_TypeDef_full_name(type_def);
+    if (flatbuffers_string_len(full_name) <= 0) {
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "types[%zu] missing name", i);
+    }
+  }
+
+  iree_vm_RodataSegmentDef_vec_t rodata_segments =
+      iree_vm_BytecodeModuleDef_rodata_segments(module_def);
+  for (size_t i = 0; i < iree_vm_RodataSegmentDef_vec_len(rodata_segments);
+       ++i) {
+    iree_vm_RodataSegmentDef_table_t segment =
+        iree_vm_RodataSegmentDef_vec_at(rodata_segments, i);
+    if (iree_vm_RodataSegmentDef_embedded_data_is_present(segment)) {
+      continue;  // embedded data is verified by FlatBuffers
+    }
+    uint64_t segment_offset =
+        iree_vm_RodataSegmentDef_external_data_offset(segment);
+    uint64_t segment_length =
+        iree_vm_RodataSegmentDef_external_data_length(segment);
+    uint64_t segment_end =
+        archive_rodata_offset + segment_offset + segment_length;
+    if (segment_end > archive_contents.data_length) {
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "rodata[%zu] external reference out of range", i);
+    }
+  }
+
+  iree_vm_ModuleDependencyDef_vec_t dependencies =
+      iree_vm_BytecodeModuleDef_dependencies(module_def);
+  for (size_t i = 0; i < iree_vm_ModuleDependencyDef_vec_len(dependencies);
+       ++i) {
+    iree_vm_ModuleDependencyDef_table_t dependency_def =
+        iree_vm_ModuleDependencyDef_vec_at(dependencies, i);
+    flatbuffers_string_t module_name =
+        iree_vm_ModuleDependencyDef_name(dependency_def);
+    if (flatbuffers_string_len(module_name) == 0) {
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "dependencies[%zu] has no module name", i);
+    }
+  }
+
+  iree_vm_ImportFunctionDef_vec_t imported_functions =
+      iree_vm_BytecodeModuleDef_imported_functions(module_def);
+  iree_vm_ExportFunctionDef_vec_t exported_functions =
+      iree_vm_BytecodeModuleDef_exported_functions(module_def);
+  iree_vm_FunctionSignatureDef_vec_t function_signatures =
+      iree_vm_BytecodeModuleDef_function_signatures(module_def);
+  iree_vm_FunctionDescriptor_vec_t function_descriptors =
+      iree_vm_BytecodeModuleDef_function_descriptors(module_def);
+
+  for (size_t i = 0;
+       i < iree_vm_FunctionSignatureDef_vec_len(function_signatures); ++i) {
+    iree_vm_FunctionSignatureDef_table_t function_signature =
+        iree_vm_FunctionSignatureDef_vec_at(function_signatures, i);
+    if (!function_signature) {
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "function_signatures[%zu] missing body", i);
+    }
+  }
+
+  for (size_t i = 0; i < iree_vm_ImportFunctionDef_vec_len(imported_functions);
+       ++i) {
+    iree_vm_ImportFunctionDef_table_t import_def =
+        iree_vm_ImportFunctionDef_vec_at(imported_functions, i);
+    if (!import_def) {
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "imports[%zu] missing body", i);
+    }
+    flatbuffers_string_t full_name =
+        iree_vm_ImportFunctionDef_full_name(import_def);
+    if (!flatbuffers_string_len(full_name)) {
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "imports[%zu] missing full_name", i);
+    }
+  }
+
+  if (iree_vm_FunctionSignatureDef_vec_len(function_signatures) !=
+      iree_vm_FunctionDescriptor_vec_len(function_descriptors)) {
+    return iree_make_status(
+        IREE_STATUS_INVALID_ARGUMENT,
+        "function signature and descriptor table length mismatch (%zu vs %zu)",
+        iree_vm_FunctionSignatureDef_vec_len(function_signatures),
+        iree_vm_FunctionDescriptor_vec_len(function_descriptors));
+  }
+
+  for (size_t i = 0; i < iree_vm_ExportFunctionDef_vec_len(exported_functions);
+       ++i) {
+    iree_vm_ExportFunctionDef_table_t export_def =
+        iree_vm_ExportFunctionDef_vec_at(exported_functions, i);
+    if (!export_def) {
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "exports[%zu] missing body", i);
+    }
+    flatbuffers_string_t local_name =
+        iree_vm_ExportFunctionDef_local_name(export_def);
+    if (!flatbuffers_string_len(local_name)) {
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "exports[%zu] missing local_name", i);
+    }
+    iree_host_size_t internal_ordinal =
+        iree_vm_ExportFunctionDef_internal_ordinal(export_def);
+    if (internal_ordinal >=
+        iree_vm_FunctionDescriptor_vec_len(function_descriptors)) {
+      return iree_make_status(
+          IREE_STATUS_INVALID_ARGUMENT,
+          "exports[%zu] internal_ordinal out of bounds (0 < %zu < %zu)", i,
+          internal_ordinal,
+          iree_vm_FunctionDescriptor_vec_len(function_descriptors));
+    }
+  }
+
+  // Verify that we can properly handle the bytecode embedded in the module.
+  // We require that major versions match and allow loading of older minor
+  // versions (we keep changes backwards-compatible).
+  const uint32_t bytecode_version =
+      iree_vm_BytecodeModuleDef_bytecode_version(module_def);
+  const uint32_t bytecode_version_major = bytecode_version >> 16;
+  const uint32_t bytecode_version_minor = bytecode_version & 0xFFFF;
+  if ((bytecode_version_major != IREE_VM_BYTECODE_VERSION_MAJOR) ||
+      (bytecode_version_minor > IREE_VM_BYTECODE_VERSION_MINOR)) {
+    return iree_make_status(
+        IREE_STATUS_INVALID_ARGUMENT,
+        "bytecode version mismatch; runtime supports %d.%d, module has %d.%d",
+        IREE_VM_BYTECODE_VERSION_MAJOR, IREE_VM_BYTECODE_VERSION_MINOR,
+        bytecode_version_major, bytecode_version_minor);
+  }
+
+  flatbuffers_uint8_vec_t bytecode_data =
+      iree_vm_BytecodeModuleDef_bytecode_data(module_def);
+  for (size_t i = 0;
+       i < iree_vm_FunctionDescriptor_vec_len(function_descriptors); ++i) {
+    iree_vm_FunctionDescriptor_struct_t function_descriptor =
+        iree_vm_FunctionDescriptor_vec_at(function_descriptors, i);
+    if (function_descriptor->block_count == 0) {
+      return iree_make_status(
+          IREE_STATUS_INVALID_ARGUMENT,
+          "functions[%zu] descriptor block count is 0; "
+          "functions must have at least 1 block, expected %d",
+          i, function_descriptor->block_count);
+    }
+    if (function_descriptor->bytecode_length == 0) {
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "functions[%zu] descriptor bytecode reports 0 "
+                              "length; functions must have at least one block",
+                              i);
+    }
+    if (function_descriptor->bytecode_offset < 0 ||
+        function_descriptor->bytecode_offset +
+                function_descriptor->bytecode_length >
+            flatbuffers_uint8_vec_len(bytecode_data)) {
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "functions[%zu] descriptor bytecode span out of "
+                              "range (0 < %d < %" PRIhsz ")",
+                              i, function_descriptor->bytecode_offset,
+                              flatbuffers_uint8_vec_len(bytecode_data));
+    }
+    if (function_descriptor->i32_register_count > IREE_I32_REGISTER_COUNT ||
+        function_descriptor->ref_register_count > IREE_REF_REGISTER_COUNT) {
+      return iree_make_status(
+          IREE_STATUS_INVALID_ARGUMENT,
+          "functions[%zu] descriptor register count out of range", i);
+    }
+  }
+
+  return iree_ok_status();
+}
+
+//===----------------------------------------------------------------------===//
+// Function verification
+//===----------------------------------------------------------------------===//
+
+// State used during verification of a function.
+typedef struct iree_vm_bytecode_verify_state_t {
+  // Within a block (encountered a block marker and not yet a terminator).
+  uint32_t in_block : 1;
+
+  // Maximum valid register ordinals.
+  uint32_t i32_register_count;
+  uint32_t ref_register_count;
+
+  // Parsed argument and result cconv fragments.
+  iree_string_view_t cconv_arguments;
+  iree_string_view_t cconv_results;
+
+  // All block branch points.
+  iree_vm_bytecode_block_list_t block_list;
+
+  // Quick lookups of flatbuffer properties.
+  const iree_vm_ImportFunctionDef_vec_t imported_functions;
+  const iree_vm_ExportFunctionDef_vec_t exported_functions;
+  const iree_vm_FunctionSignatureDef_vec_t function_signatures;
+  const iree_vm_FunctionDescriptor_vec_t function_descriptors;
+  iree_host_size_t rodata_storage_size;
+  iree_host_size_t rodata_ref_count;
+  iree_host_size_t rwdata_storage_size;
+  iree_host_size_t global_ref_count;
+} iree_vm_bytecode_verify_state_t;
+
+// Parses the cconv fragments from the given |signature_def|.
+static iree_status_t iree_vm_bytecode_function_get_cconv_fragments(
+    iree_vm_FunctionSignatureDef_table_t signature_def,
+    iree_string_view_t* out_arguments, iree_string_view_t* out_results);
+
+// Verifies that the function has storage for the declared arguments.
+static iree_status_t iree_vm_bytecode_function_verify_arguments(
+    const iree_vm_bytecode_verify_state_t* verify_state);
+
+// Verifies a single operation at |pc| in the function |bytecode_data|.
+// Returns an error if the op is invalid and otherwise sets |out_next_pc| to the
+// program counter immediately following the op (which may be the end of data!).
+static iree_status_t iree_vm_bytecode_function_verify_bytecode_op(
+    iree_vm_bytecode_module_t* module,
+    iree_vm_bytecode_verify_state_t* verify_state,
+    iree_vm_FunctionSignatureDef_table_t function_signature,
+    iree_vm_FunctionDescriptor_struct_t function_descriptor,
+    iree_const_byte_span_t bytecode_data, uint32_t pc, uint32_t max_pc,
+    uint32_t* out_next_pc);
+
+// NOTE: by the time this is called we have the module created and can assume
+// all information on it has been verified. The only thing this verifies is
+// function bytecode and capabilities!
+iree_status_t iree_vm_bytecode_function_verify(
+    iree_vm_bytecode_module_t* module, uint16_t function_ordinal,
+    iree_allocator_t scratch_allocator) {
+  if (function_ordinal >= module->function_descriptor_count) {
+    return iree_make_status(IREE_STATUS_OUT_OF_RANGE,
+                            "invalid function ordinal");
+  }
+  iree_vm_FunctionSignatureDef_table_t function_signature_def =
+      iree_vm_FunctionSignatureDef_vec_at(
+          iree_vm_BytecodeModuleDef_function_signatures(module->def),
+          function_ordinal);
+  const iree_vm_FunctionDescriptor_t* function_descriptor =
+      &module->function_descriptor_table[function_ordinal];
+
+  const iree_vm_FeatureBits_enum_t available_features =
+      iree_vm_bytecode_available_features();
+  const iree_vm_FeatureBits_enum_t required_features =
+      function_descriptor->requirements;
+  IREE_RETURN_IF_ERROR(iree_vm_check_feature_mismatch(
+      __FILE__, __LINE__, required_features, available_features));
+
+  if (function_descriptor->block_count == 0) {
+    return iree_make_status(
+        IREE_STATUS_OUT_OF_RANGE,
+        "no blocks defined; functions must have at least one block");
+  }
+
+  // State used during verification.
+  iree_vm_bytecode_verify_state_t verify_state = {
+      .in_block = 0,
+      .imported_functions =
+          iree_vm_BytecodeModuleDef_imported_functions(module->def),
+      .exported_functions =
+          iree_vm_BytecodeModuleDef_exported_functions(module->def),
+      .function_signatures =
+          iree_vm_BytecodeModuleDef_function_signatures(module->def),
+      .function_descriptors =
+          iree_vm_BytecodeModuleDef_function_descriptors(module->def),
+      .rodata_storage_size = 0,
+      .rodata_ref_count = 0,
+      .rwdata_storage_size = 0,
+      .global_ref_count = 0,
+  };
+
+  // NOTE: these must be consistent with iree_vm_bytecode_module_layout_state.
+  verify_state.rodata_storage_size = 0;
+  verify_state.rodata_ref_count = iree_vm_RodataSegmentDef_vec_len(
+      iree_vm_BytecodeModuleDef_rodata_segments(module->def));
+  iree_vm_ModuleStateDef_table_t module_state_def =
+      iree_vm_BytecodeModuleDef_module_state(module->def);
+  if (module_state_def) {
+    verify_state.rwdata_storage_size =
+        iree_vm_ModuleStateDef_global_bytes_capacity(module_state_def);
+    verify_state.global_ref_count =
+        iree_vm_ModuleStateDef_global_ref_count(module_state_def);
+  }
+
+  // Ensure the register storage (rounded to the nearest power of 2) won't
+  // exceed the maximum allowed registers.
+  verify_state.i32_register_count = iree_math_round_up_to_pow2_u32(
+      VMMAX(1, function_descriptor->i32_register_count));
+  verify_state.ref_register_count = iree_math_round_up_to_pow2_u32(
+      VMMAX(1, function_descriptor->ref_register_count));
+  if (IREE_UNLIKELY(verify_state.i32_register_count > IREE_I32_REGISTER_MASK) ||
+      IREE_UNLIKELY(verify_state.ref_register_count > IREE_REF_REGISTER_MASK)) {
+    // Register count overflow. A valid compiler should never produce files that
+    // hit this.
+    return iree_make_status(IREE_STATUS_RESOURCE_EXHAUSTED,
+                            "register count overflow");
+  }
+
+  // Grab the cconv fragments declaring the arguments/results of the function.
+  verify_state.cconv_arguments = iree_string_view_empty();
+  verify_state.cconv_results = iree_string_view_empty();
+  IREE_RETURN_IF_ERROR(iree_vm_bytecode_function_get_cconv_fragments(
+      function_signature_def, &verify_state.cconv_arguments,
+      &verify_state.cconv_results));
+
+  // Verify there is storage for passed arguments.
+  IREE_RETURN_IF_ERROR(
+      iree_vm_bytecode_function_verify_arguments(&verify_state));
+
+  // NOTE: module verification ensures the function descriptor has a valid
+  // bytecode range so we can assume that's true here.
+  IREE_ASSERT(function_descriptor->bytecode_length > 0);
+  IREE_ASSERT(function_descriptor->bytecode_offset >= 0);
+  IREE_ASSERT(function_descriptor->bytecode_offset +
+                  function_descriptor->bytecode_length <=
+              module->bytecode_data.data_length);
+  iree_const_byte_span_t bytecode_data = iree_make_const_byte_span(
+      module->bytecode_data.data + function_descriptor->bytecode_offset,
+      function_descriptor->bytecode_length);
+  const uint32_t max_pc = (uint32_t)function_descriptor->bytecode_length;
+
+  // Reserve the block list. As we walk the bytecode we'll declare/define blocks
+  // and then afterward verify all were found.
+  IREE_ASSERT(function_descriptor->block_count > 0);
+  IREE_RETURN_IF_ERROR(iree_vm_bytecode_block_list_initialize(
+      function_descriptor->block_count, scratch_allocator,
+      &verify_state.block_list));
+
+  // Perform bytecode verification by performing a single-pass walk of all
+  // function bytecode.
+  iree_status_t status = iree_ok_status();
+  for (uint32_t pc = 0; pc < bytecode_data.data_length - 1;) {
+    uint32_t start_pc = pc;
+    status = iree_vm_bytecode_function_verify_bytecode_op(
+        module, &verify_state, function_signature_def, function_descriptor,
+        bytecode_data, start_pc, max_pc, &pc);
+    if (!iree_status_is_ok(status)) {
+#if IREE_STATUS_MODE
+      // To get a useful source location we have to ask the main module; the
+      // base function table may only contain public symbols and not any
+      // internal ones.
+      iree_string_view_t module_name = iree_vm_module_name(&module->interface);
+      iree_vm_function_t function = {0};
+      iree_status_ignore(iree_vm_module_lookup_function_by_ordinal(
+          &module->interface, IREE_VM_FUNCTION_LINKAGE_INTERNAL,
+          function_ordinal, &function));
+      iree_string_view_t function_name = iree_vm_function_name(&function);
+      if (!iree_string_view_is_empty(function_name)) {
+        status = iree_status_annotate_f(status, "at %.*s.%.*s+%08X",
+                                        (int)module_name.size, module_name.data,
+                                        (int)function_name.size,
+                                        function_name.data, start_pc);
+      } else {
+        status = iree_status_annotate_f(status, "at %.*s@%u+%08X",
+                                        (int)module_name.size, module_name.data,
+                                        function_ordinal, start_pc);
+      }
+#endif  // IREE_STATUS_MODE
+      break;
+    }
+  }
+
+  // Ensure there was a terminator.
+  if (iree_status_is_ok(status) && verify_state.in_block) {
+    status = iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "function missing terminator in the last block");
+  }
+
+  // Verify all blocks are defined and have proper markers.
+  if (iree_status_is_ok(status)) {
+    status = iree_vm_bytecode_block_list_verify(&verify_state.block_list,
+                                                bytecode_data);
+  }
+
+  iree_vm_bytecode_block_list_deinitialize(&verify_state.block_list,
+                                           scratch_allocator);
+
+  return status;
+}
+
+//===----------------------------------------------------------------------===//
+// Utilities matching the tablegen op encoding scheme
+//===----------------------------------------------------------------------===//
+// These utilities match the VM_Enc* statements in VMBase.td 1:1, allowing us
+// to have the inverse of the encoding which make things easier to read.
+//
+// Each macro will increment the pc by the number of bytes read and as such must
+// be called in the same order the values are encoded.
+
+// Bails if the |pc| exceeds the |max_pc|.
+#define IREE_VM_VERIFY_PC_RANGE(pc, max_pc)                                  \
+  if (IREE_UNLIKELY((pc) > (max_pc))) {                                      \
+    return iree_make_status(IREE_STATUS_OUT_OF_RANGE,                        \
+                            "bytecode data overrun trying to parsing op at " \
+                            "%08X (%u) of %u available bytes",               \
+                            (uint32_t)(pc), (uint32_t)(pc),                  \
+                            (uint32_t)(max_pc));                             \
+  }
+
+// Bails if the function doesn't have the given |required_features| declared.
+#define IREE_VM_VERIFY_REQUIREMENT(required_features)                         \
+  if (IREE_UNLIKELY(!iree_all_bits_set(function_descriptor->requirements,     \
+                                       (required_features)))) {               \
+    return iree_vm_check_feature_mismatch(__FILE__, __LINE__,                 \
+                                          (required_features),                \
+                                          function_descriptor->requirements); \
+  }
+
+// Bails if the register ordinal for the given register type is out of bounds.
+#define IREE_VM_VERIFY_REG_ORDINAL(name)                            \
+  IREE_VM_VERIFY_PC_RANGE(pc + IREE_REGISTER_ORDINAL_SIZE, max_pc); \
+  const uint32_t name = OP_I16(0);
+#define IREE_VM_VERIFY_REG_ORDINAL_X32(ordinal, category)                      \
+  if (IREE_UNLIKELY(((ordinal)&IREE_REF_REGISTER_TYPE_BIT) != 0)) {            \
+    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,                      \
+                            category                                           \
+                            " register required but ref register %u provided", \
+                            (ordinal));                                        \
+  } else if (IREE_UNLIKELY((ordinal) >= verify_state->i32_register_count)) {   \
+    return iree_make_status(IREE_STATUS_OUT_OF_RANGE,                          \
+                            category " register ordinal %u out of range %u",   \
+                            (ordinal), verify_state->i32_register_count);      \
+  }
+#define IREE_VM_VERIFY_REG_ORDINAL_X64(ordinal, category)                      \
+  if (IREE_UNLIKELY(((ordinal)&IREE_REF_REGISTER_TYPE_BIT) != 0)) {            \
+    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,                      \
+                            category                                           \
+                            " register required but ref register %u provided", \
+                            (ordinal));                                        \
+  } else if (IREE_UNLIKELY((ordinal & 1) != 0)) {                              \
+    return iree_make_status(                                                   \
+        IREE_STATUS_INVALID_ARGUMENT,                                          \
+        category " register ordinal %u not 8-byte aligned", (ordinal));        \
+  } else if (IREE_UNLIKELY((ordinal) + 1 >=                                    \
+                           verify_state->i32_register_count)) {                \
+    return iree_make_status(                                                   \
+        IREE_STATUS_OUT_OF_RANGE,                                              \
+        category " register ordinal %u:%u out of range %u", (ordinal),         \
+        (ordinal) + 1, verify_state->i32_register_count);                      \
+  }
+#define IREE_VM_VERIFY_REG_I32(ordinal) \
+  IREE_VM_VERIFY_REG_ORDINAL_X32(ordinal, "i32");
+#define IREE_VM_VERIFY_REG_I64(ordinal) \
+  IREE_VM_VERIFY_REG_ORDINAL_X64(ordinal, "i64");
+#define IREE_VM_VERIFY_REG_F32(ordinal) \
+  IREE_VM_VERIFY_REG_ORDINAL_X32(ordinal, "f32");
+#define IREE_VM_VERIFY_REG_F64(ordinal) \
+  IREE_VM_VERIFY_REG_ORDINAL_X64(ordinal, "f64");
+#define IREE_VM_VERIFY_REG_REF(ordinal)                                      \
+  if (IREE_UNLIKELY(((ordinal)&IREE_REF_REGISTER_TYPE_BIT) == 0)) {          \
+    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,                    \
+                            "ref register required but non-ref %u provided", \
+                            (ordinal));                                      \
+  } else if (IREE_UNLIKELY(((ordinal)&IREE_REF_REGISTER_MASK) >=             \
+                           verify_state->ref_register_count)) {              \
+    return iree_make_status(IREE_STATUS_OUT_OF_RANGE,                        \
+                            "ref register ordinal %u out of range %u",       \
+                            (ordinal), verify_state->ref_register_count);    \
+  }
+#define IREE_VM_VERIFY_REG_ANY(ordinal)                             \
+  if (IREE_UNLIKELY(((ordinal)&IREE_REF_REGISTER_TYPE_BIT) == 0)) { \
+  } else {                                                          \
+  }
+
+#define VM_VerifyConstI8(name)             \
+  IREE_VM_VERIFY_PC_RANGE(pc + 1, max_pc); \
+  uint8_t name = OP_I8(0);                 \
+  (void)(name);                            \
+  ++pc;
+#define VM_VerifyConstI32(name)            \
+  IREE_VM_VERIFY_PC_RANGE(pc + 4, max_pc); \
+  uint32_t name = OP_I32(0);               \
+  (void)(name);                            \
+  pc += 4;
+#define VM_VerifyConstI64(name)            \
+  IREE_VM_VERIFY_PC_RANGE(pc + 8, max_pc); \
+  uint32_t name = OP_I64(0);               \
+  (void)(name);                            \
+  pc += 8;
+#define VM_VerifyConstF32(name)            \
+  IREE_VM_VERIFY_PC_RANGE(pc + 4, max_pc); \
+  uint32_t name = OP_F32(0);               \
+  (void)(name);                            \
+  pc += 4;
+#define VM_VerifyConstF64(name)            \
+  IREE_VM_VERIFY_PC_RANGE(pc + 8, max_pc); \
+  uint32_t name = OP_F64(0);               \
+  (void)(name);                            \
+  pc += 8;
+
+#define VM_VerifyFuncAttr(name) VM_VerifyConstI32(name)
+#define VM_IsImportOrdinal(name) (((name)&0x80000000u) != 0)
+#define VM_UnmaskImportOrdinal(name) name &= ~0x80000000u
+#define VM_VerifyImportOrdinal(name)                                          \
+  if (IREE_UNLIKELY((name) >= iree_vm_ImportFunctionDef_vec_len(              \
+                                  verify_state->imported_functions))) {       \
+    return iree_make_status(                                                  \
+        IREE_STATUS_INVALID_ARGUMENT, "import ordinal %u out of range %zu",   \
+        name,                                                                 \
+        iree_vm_ImportFunctionDef_vec_len(verify_state->imported_functions)); \
+  }
+#define VM_VerifyFunctionOrdinal(name)                                      \
+  if (IREE_UNLIKELY((name)) >= iree_vm_FunctionDescriptor_vec_len(          \
+                                   verify_state->function_descriptors)) {   \
+    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,                   \
+                            "function ordinal %u out of range %zu", (name), \
+                            iree_vm_FunctionDescriptor_vec_len(             \
+                                verify_state->function_descriptors));       \
+  }
+#define VM_VerifyGlobalAttr(name) VM_VerifyConstI32(name)
+#define VM_VerifyRwdataOffset(name, access_length)                  \
+  if (IREE_UNLIKELY(((name) + (access_length)) >                    \
+                    verify_state->rwdata_storage_size)) {           \
+    return iree_make_status(                                        \
+        IREE_STATUS_OUT_OF_RANGE,                                   \
+        "global byte_offset out of range: %d (rwdata=%zu)", (name), \
+        verify_state->rwdata_storage_size);                         \
+  }
+#define VM_VerifyGlobalRefOrdinal(name)                                        \
+  if (IREE_UNLIKELY((name) >= verify_state->global_ref_count)) {               \
+    return iree_make_status(IREE_STATUS_OUT_OF_RANGE,                          \
+                            "global ref ordinal out of range: %d (table=%zu)", \
+                            (name), verify_state->global_ref_count);           \
+  }
+#define VM_VerifyRodataAttr(name) VM_VerifyConstI32(name)
+#define VM_VerifyRodataOrdinal(name)                                           \
+  if (IREE_UNLIKELY((name) >= verify_state->rodata_ref_count)) {               \
+    return iree_make_status(IREE_STATUS_OUT_OF_RANGE,                          \
+                            "rodata ref ordinal out of range: %d (table=%zu)", \
+                            (name), verify_state->rodata_ref_count);           \
+  }
+#define VM_VerifyType(name)                                                 \
+  IREE_VM_VERIFY_PC_RANGE(pc + 4, max_pc);                                  \
+  uint32_t name##_id = OP_I32(0);                                           \
+  if (IREE_UNLIKELY(name##_id >= module->type_count)) {                     \
+    return iree_make_status(IREE_STATUS_OUT_OF_RANGE,                       \
+                            "type id ordinal out of range: %d (table=%zu)", \
+                            name##_id, module->type_count);                 \
+  }                                                                         \
+  const iree_vm_type_def_t* name = &module->type_table[name##_id];          \
+  (void)(name);                                                             \
+  pc += 4;
+#define VM_VerifyTypeOf(name) VM_VerifyType(name)
+#define VM_VerifyIntAttr32(name) VM_VerifyConstI32(name)
+#define VM_VerifyIntAttr64(name) VM_VerifyConstI64(name)
+#define VM_VerifyFloatAttr32(name) VM_VerifyConstF32(name)
+#define VM_VerifyFloatAttr64(name) VM_VerifyConstF64(name)
+#define VM_VerifyStrAttr(name, out_str)                      \
+  IREE_VM_VERIFY_PC_RANGE(pc + 2, max_pc);                   \
+  (out_str)->size = (iree_host_size_t)OP_I16(0);             \
+  IREE_VM_VERIFY_PC_RANGE(pc + 2 + (out_str)->size, max_pc); \
+  (out_str)->data = (const char*)&bytecode_data[pc + 2];     \
+  pc += 2 + (out_str)->size;
+
+#define VM_VerifyBranchTarget(name)                        \
+  VM_VerifyConstI32(name##_pc);                            \
+  iree_vm_bytecode_block_t* name = NULL;                   \
+  IREE_RETURN_IF_ERROR(iree_vm_bytecode_block_list_insert( \
+      &verify_state->block_list, name##_pc, &name));
+#define VM_VerifyBranchOperands(name)                                         \
+  VM_AlignPC(pc, IREE_REGISTER_ORDINAL_SIZE);                                 \
+  IREE_VM_VERIFY_PC_RANGE(pc + IREE_REGISTER_ORDINAL_SIZE, max_pc);           \
+  const iree_vm_register_remap_list_t* name =                                 \
+      (const iree_vm_register_remap_list_t*)&bytecode_data[pc];               \
+  pc += IREE_REGISTER_ORDINAL_SIZE;                                           \
+  IREE_VM_VERIFY_PC_RANGE(pc + (name)->size * 2 * IREE_REGISTER_ORDINAL_SIZE, \
+                          max_pc);                                            \
+  pc += (name)->size * 2 * IREE_REGISTER_ORDINAL_SIZE;                        \
+  for (uint16_t i = 0; i < name->size; ++i) {                                 \
+    IREE_VM_VERIFY_REG_ANY(name->pairs[i].src_reg);                           \
+    IREE_VM_VERIFY_REG_ANY(name->pairs[i].dst_reg);                           \
+  }
+
+#define VM_VerifyOperandRegI32(name)          \
+  IREE_VM_VERIFY_REG_ORDINAL(name##_ordinal); \
+  IREE_VM_VERIFY_REG_I32(name##_ordinal);     \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_VerifyOperandRegI64(name)          \
+  IREE_VM_VERIFY_REG_ORDINAL(name##_ordinal); \
+  IREE_VM_VERIFY_REG_I64(name##_ordinal);     \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_VerifyOperandRegI64HostSize(name) VM_VerifyOperandRegI64(name)
+#define VM_VerifyOperandRegF32(name)          \
+  IREE_VM_VERIFY_REG_ORDINAL(name##_ordinal); \
+  IREE_VM_VERIFY_REG_F32(name##_ordinal);     \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_VerifyOperandRegF64(name)          \
+  IREE_VM_VERIFY_REG_ORDINAL(name##_ordinal); \
+  IREE_VM_VERIFY_REG_F32(name##_ordinal);     \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_VerifyOperandRegRef(name)          \
+  IREE_VM_VERIFY_REG_ORDINAL(name##_ordinal); \
+  IREE_VM_VERIFY_REG_REF(name##_ordinal);     \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_VerifyVariadicOperands(name)                                   \
+  VM_AlignPC(pc, IREE_REGISTER_ORDINAL_SIZE);                             \
+  IREE_VM_VERIFY_PC_RANGE(pc + IREE_REGISTER_ORDINAL_SIZE, max_pc);       \
+  const iree_vm_register_list_t* name =                                   \
+      (const iree_vm_register_list_t*)&bytecode_data[pc];                 \
+  pc += IREE_REGISTER_ORDINAL_SIZE;                                       \
+  IREE_VM_VERIFY_PC_RANGE(pc + (name)->size * IREE_REGISTER_ORDINAL_SIZE, \
+                          max_pc);                                        \
+  pc += (name)->size * IREE_REGISTER_ORDINAL_SIZE;
+#define VM_VerifyVariadicOperandsI32(name)            \
+  VM_VerifyVariadicOperands(name);                    \
+  for (uint16_t __i = 0; __i < (name)->size; ++__i) { \
+    IREE_VM_VERIFY_REG_I32((name)->registers[__i]);   \
+  }
+#define VM_VerifyVariadicOperandsI64(name)            \
+  VM_VerifyVariadicOperands(name);                    \
+  for (uint16_t __i = 0; __i < (name)->size; ++__i) { \
+    IREE_VM_VERIFY_REG_I64((name)->registers[__i]);   \
+  }
+#define VM_VerifyVariadicOperandsF32(name)            \
+  VM_VerifyVariadicOperands(name);                    \
+  for (uint16_t __i = 0; __i < (name)->size; ++__i) { \
+    IREE_VM_VERIFY_REG_F32((name)->registers[__i]);   \
+  }
+#define VM_VerifyVariadicOperandsF64(name)            \
+  VM_VerifyVariadicOperands(name);                    \
+  for (uint16_t __i = 0; __i < (name)->size; ++__i) { \
+    IREE_VM_VERIFY_REG_F64((name)->registers[__i]);   \
+  }
+#define VM_VerifyVariadicOperandsRef(name, type_def)  \
+  VM_VerifyVariadicOperands(name);                    \
+  for (uint16_t __i = 0; __i < (name)->size; ++__i) { \
+    IREE_VM_VERIFY_REG_REF((name)->registers[__i]);   \
+  }
+#define VM_VerifyVariadicOperandsAny(name)            \
+  VM_VerifyVariadicOperands(name);                    \
+  for (uint16_t __i = 0; __i < (name)->size; ++__i) { \
+    IREE_VM_VERIFY_REG_ANY((name)->registers[__i]);   \
+  }
+#define VM_VerifyResultRegI32(name)           \
+  IREE_VM_VERIFY_REG_ORDINAL(name##_ordinal); \
+  IREE_VM_VERIFY_REG_I32(name##_ordinal);     \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_VerifyResultRegI64(name)           \
+  IREE_VM_VERIFY_REG_ORDINAL(name##_ordinal); \
+  IREE_VM_VERIFY_REG_I64(name##_ordinal);     \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_VerifyResultRegF32(name)           \
+  IREE_VM_VERIFY_REG_ORDINAL(name##_ordinal); \
+  IREE_VM_VERIFY_REG_F32(name##_ordinal);     \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_VerifyResultRegF64(name)           \
+  IREE_VM_VERIFY_REG_ORDINAL(name##_ordinal); \
+  IREE_VM_VERIFY_REG_F64(name##_ordinal);     \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_VerifyResultRegRef(name)           \
+  IREE_VM_VERIFY_REG_ORDINAL(name##_ordinal); \
+  IREE_VM_VERIFY_REG_REF(name##_ordinal);     \
+  pc += IREE_REGISTER_ORDINAL_SIZE;
+#define VM_VerifyVariadicResultsAny(name) VM_VerifyVariadicOperandsAny(name)
+
+#define VERIFY_OP_CORE_UNARY_I32(op_name) \
+  VERIFY_OP(CORE, op_name, {              \
+    VM_VerifyOperandRegI32(operand);      \
+    VM_VerifyResultRegI32(result);        \
+  });
+
+#define VERIFY_OP_CORE_UNARY_I64(op_name) \
+  VERIFY_OP(CORE, op_name, {              \
+    VM_VerifyOperandRegI64(operand);      \
+    VM_VerifyResultRegI64(result);        \
+  });
+
+#define VERIFY_OP_CORE_BINARY_I32(op_name) \
+  VERIFY_OP(CORE, op_name, {               \
+    VM_VerifyOperandRegI32(lhs);           \
+    VM_VerifyOperandRegI32(rhs);           \
+    VM_VerifyResultRegI32(result);         \
+  });
+
+#define VERIFY_OP_CORE_BINARY_I64(op_name) \
+  VERIFY_OP(CORE, op_name, {               \
+    VM_VerifyOperandRegI64(lhs);           \
+    VM_VerifyOperandRegI64(rhs);           \
+    VM_VerifyResultRegI64(result);         \
+  });
+
+#define VERIFY_OP_CORE_TERNARY_I32(op_name) \
+  VERIFY_OP(CORE, op_name, {                \
+    VM_VerifyOperandRegI32(a);              \
+    VM_VerifyOperandRegI32(b);              \
+    VM_VerifyOperandRegI32(c);              \
+    VM_VerifyResultRegI32(result);          \
+  });
+
+#define VERIFY_OP_CORE_TERNARY_I64(op_name) \
+  VERIFY_OP(CORE, op_name, {                \
+    VM_VerifyOperandRegI64(a);              \
+    VM_VerifyOperandRegI64(b);              \
+    VM_VerifyOperandRegI64(c);              \
+    VM_VerifyResultRegI64(result);          \
+  });
+
+#define VERIFY_OP_EXT_F32_UNARY_F32(op_name) \
+  VERIFY_OP(EXT_F32, op_name, {              \
+    VM_VerifyOperandRegF32(operand);         \
+    VM_VerifyResultRegF32(result);           \
+  });
+
+#define VERIFY_OP_EXT_F32_BINARY_F32(op_name) \
+  VERIFY_OP(EXT_F32, op_name, {               \
+    VM_VerifyOperandRegF32(lhs);              \
+    VM_VerifyOperandRegF32(rhs);              \
+    VM_VerifyResultRegF32(result);            \
+  });
+
+#define VERIFY_OP_EXT_F32_TERNARY_F32(op_name) \
+  VERIFY_OP(EXT_F32, op_name, {                \
+    VM_VerifyOperandRegF32(a);                 \
+    VM_VerifyOperandRegF32(b);                 \
+    VM_VerifyOperandRegF32(c);                 \
+    VM_VerifyResultRegF32(result);             \
+  });
+
+#define VERIFY_OP_EXT_F64_UNARY_F64(op_name) \
+  VERIFY_OP(EXT_F64, op_name, {              \
+    VM_VerifyOperandRegF64(operand);         \
+    VM_VerifyResultRegF64(result);           \
+  });
+
+#define VERIFY_OP_EXT_F64_BINARY_F64(op_name) \
+  VERIFY_OP(EXT_F64, op_name, {               \
+    VM_VerifyOperandRegF64(lhs);              \
+    VM_VerifyOperandRegF64(rhs);              \
+    VM_VerifyResultRegF64(result);            \
+  });
+
+#define VERIFY_OP_EXT_F64_TERNARY_F64(op_name) \
+  VERIFY_OP(EXT_F64, op_name, {                \
+    VM_VerifyOperandRegF64(a);                 \
+    VM_VerifyOperandRegF64(b);                 \
+    VM_VerifyOperandRegF64(c);                 \
+    VM_VerifyResultRegF64(result);             \
+  });
+
+//===----------------------------------------------------------------------===//
+// Call verification
+//===----------------------------------------------------------------------===//
+
+static iree_status_t iree_vm_bytecode_function_get_cconv_fragments(
+    iree_vm_FunctionSignatureDef_table_t signature_def,
+    iree_string_view_t* out_arguments, iree_string_view_t* out_results) {
+  flatbuffers_string_t cconv_str =
+      iree_vm_FunctionSignatureDef_calling_convention(signature_def);
+  iree_vm_function_signature_t signature = {
+      .calling_convention =
+          iree_make_string_view(cconv_str, flatbuffers_string_len(cconv_str)),
+  };
+  return iree_vm_function_call_get_cconv_fragments(&signature, out_arguments,
+                                                   out_results);
+}
+
+static iree_status_t iree_vm_bytecode_function_count_cconv_regs(
+    iree_string_view_t cconv_fragment, iree_host_size_t* out_i32_count,
+    iree_host_size_t* out_ref_count) {
+  iree_host_size_t i32_count = 0;
+  iree_host_size_t ref_count = 0;
+  for (iree_host_size_t i = 0; i < cconv_fragment.size; ++i) {
+    switch (cconv_fragment.data[i]) {
+      default:
+        return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                                "unsupported cconv fragment char '%c'",
+                                cconv_fragment.data[i]);
+      case IREE_VM_CCONV_TYPE_VOID:
+        break;
+      case IREE_VM_CCONV_TYPE_I32:
+      case IREE_VM_CCONV_TYPE_F32:
+        ++i32_count;
+        break;
+      case IREE_VM_CCONV_TYPE_I64:
+      case IREE_VM_CCONV_TYPE_F64:
+        if ((i32_count % 2) != 0) {
+          // Unaligned; pad an i32 register to get to i64 alignment.
+          ++i32_count;
+        }
+        i32_count += 2;
+        break;
+      case IREE_VM_CCONV_TYPE_REF:
+        ++ref_count;
+        break;
+      case IREE_VM_CCONV_TYPE_SPAN_START:
+        return iree_make_status(
+            IREE_STATUS_INVALID_ARGUMENT,
+            "internal functions cannot accept variadic arguments");
+    }
+  }
+  *out_i32_count = i32_count;
+  *out_ref_count = ref_count;
+  return iree_ok_status();
+}
+
+static iree_status_t iree_vm_bytecode_function_verify_arguments(
+    const iree_vm_bytecode_verify_state_t* verify_state) {
+  iree_host_size_t args_i32 = 0;
+  iree_host_size_t args_ref = 0;
+  IREE_RETURN_IF_ERROR(iree_vm_bytecode_function_count_cconv_regs(
+      verify_state->cconv_arguments, &args_i32, &args_ref));
+  iree_host_size_t rets_i32 = 0;
+  iree_host_size_t rets_ref = 0;
+  IREE_RETURN_IF_ERROR(iree_vm_bytecode_function_count_cconv_regs(
+      verify_state->cconv_results, &rets_i32, &rets_ref));
+  if (verify_state->i32_register_count < args_i32 ||
+      verify_state->i32_register_count < rets_i32 ||
+      verify_state->ref_register_count < args_ref ||
+      verify_state->ref_register_count < rets_ref) {
+    return iree_make_status(
+        IREE_STATUS_INVALID_ARGUMENT,
+        "insufficient register storage for function arguments/results");
+  }
+  return iree_ok_status();
+}
+
+static iree_status_t iree_vm_bytecode_function_verify_cconv_register(
+    const iree_vm_bytecode_verify_state_t* verify_state, char cconv_type,
+    const iree_vm_register_list_t* IREE_RESTRICT reg_list, int reg_i) {
+  if (reg_i >= reg_list->size) {
+    return iree_make_status(
+        IREE_STATUS_OUT_OF_RANGE,
+        "register list underflow (have %u, trying to access %u)",
+        reg_list->size, reg_i);
+  }
+  switch (cconv_type) {
+    case IREE_VM_CCONV_TYPE_I32:
+    case IREE_VM_CCONV_TYPE_F32: {
+      IREE_VM_VERIFY_REG_ORDINAL_X32(reg_list->registers[reg_i], "i32/f32");
+    } break;
+    case IREE_VM_CCONV_TYPE_I64:
+    case IREE_VM_CCONV_TYPE_F64: {
+      IREE_VM_VERIFY_REG_ORDINAL_X64(reg_list->registers[reg_i], "i64/f64");
+    } break;
+    case IREE_VM_CCONV_TYPE_REF: {
+      IREE_VM_VERIFY_REG_REF(reg_list->registers[reg_i]);
+    } break;
+    default:
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "unsupported cconv fragment char '%c'",
+                              cconv_type);
+  }
+  return iree_ok_status();
+}
+
+static iree_status_t iree_vm_bytecode_function_verify_cconv_registers(
+    const iree_vm_bytecode_verify_state_t* verify_state,
+    iree_string_view_t cconv_fragment,
+    const iree_vm_register_list_t* IREE_RESTRICT segment_size_list,
+    const iree_vm_register_list_t* IREE_RESTRICT reg_list) {
+  for (uint16_t i = 0, seg_i = 0, reg_i = 0; i < cconv_fragment.size;
+       ++i, ++seg_i) {
+    switch (cconv_fragment.data[i]) {
+      case IREE_VM_CCONV_TYPE_VOID:
+        break;
+      case IREE_VM_CCONV_TYPE_I32:
+      case IREE_VM_CCONV_TYPE_F32:
+      case IREE_VM_CCONV_TYPE_I64:
+      case IREE_VM_CCONV_TYPE_F64:
+      case IREE_VM_CCONV_TYPE_REF: {
+        IREE_RETURN_IF_ERROR(iree_vm_bytecode_function_verify_cconv_register(
+            verify_state, cconv_fragment.data[i], reg_list, reg_i++));
+      } break;
+      case IREE_VM_CCONV_TYPE_SPAN_START: {
+        if (!segment_size_list) {
+          return iree_make_status(
+              IREE_STATUS_INVALID_ARGUMENT,
+              "function is variadic but no segment size list provided");
+        } else if (seg_i >= segment_size_list->size) {
+          return iree_make_status(
+              IREE_STATUS_OUT_OF_RANGE,
+              "segment size list underflow (have %u, trying to access %u)",
+              segment_size_list->size, seg_i);
+        }
+        uint16_t span_count = segment_size_list->registers[seg_i];
+        if (!span_count) {
+          // No items; skip the span.
+          do {
+            ++i;
+          } while (i < cconv_fragment.size &&
+                   cconv_fragment.data[i] != IREE_VM_CCONV_TYPE_SPAN_END);
+          continue;
+        }
+        uint16_t span_start_i = i + 1;
+        for (uint16_t j = 0; j < span_count; ++j) {
+          for (i = span_start_i;
+               i < cconv_fragment.size &&
+               cconv_fragment.data[i] != IREE_VM_CCONV_TYPE_SPAN_END;
+               ++i) {
+            // TODO(benvanik): share with switch above.
+            switch (cconv_fragment.data[i]) {
+              case IREE_VM_CCONV_TYPE_VOID:
+                break;
+              case IREE_VM_CCONV_TYPE_I32:
+              case IREE_VM_CCONV_TYPE_F32:
+              case IREE_VM_CCONV_TYPE_I64:
+              case IREE_VM_CCONV_TYPE_F64:
+              case IREE_VM_CCONV_TYPE_REF: {
+                IREE_RETURN_IF_ERROR(
+                    iree_vm_bytecode_function_verify_cconv_register(
+                        verify_state, cconv_fragment.data[i], reg_list,
+                        reg_i++));
+              } break;
+              default:
+                return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                                        "unsupported cconv fragment char '%c'",
+                                        cconv_fragment.data[i]);
+            }
+          }
+        }
+      } break;
+      default:
+        return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                                "unsupported cconv fragment char '%c'",
+                                cconv_fragment.data[i]);
+    }
+  }
+  return iree_ok_status();
+}
+
+static iree_status_t iree_vm_bytecode_function_verify_call(
+    const iree_vm_bytecode_verify_state_t* verify_state,
+    iree_vm_FunctionSignatureDef_table_t signature_def,
+    const iree_vm_register_list_t* IREE_RESTRICT segment_size_list,
+    const iree_vm_register_list_t* IREE_RESTRICT src_reg_list,
+    const iree_vm_register_list_t* IREE_RESTRICT dst_reg_list) {
+  iree_string_view_t arguments = iree_string_view_empty();
+  iree_string_view_t results = iree_string_view_empty();
+  IREE_RETURN_IF_ERROR(iree_vm_bytecode_function_get_cconv_fragments(
+      signature_def, &arguments, &results));
+  IREE_RETURN_IF_ERROR(iree_vm_bytecode_function_verify_cconv_registers(
+      verify_state, arguments, segment_size_list, src_reg_list));
+  IREE_RETURN_IF_ERROR(iree_vm_bytecode_function_verify_cconv_registers(
+      verify_state, results, /*segment_sizes=*/NULL, dst_reg_list));
+  return iree_ok_status();
+}
+
+//===----------------------------------------------------------------------===//
+// Bytecode verification
+//===----------------------------------------------------------------------===//
+
+#define VERIFY_OP(ext, op_name, body)  \
+  case IREE_VM_OP_##ext##_##op_name: { \
+    body;                              \
+  } break;
+
+#define BEGIN_VERIFY_PREFIX(op_name, ext)    \
+  case IREE_VM_OP_CORE_##op_name: {          \
+    IREE_VM_VERIFY_PC_RANGE(pc + 1, max_pc); \
+    IREE_VM_VERIFY_REQUIREMENT(ext);         \
+    switch (bytecode_data[pc++]) {
+#define END_VERIFY_PREFIX()                               \
+  default:                                                \
+    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, \
+                            "unhandled ext opcode");      \
+    }                                                     \
+    break;                                                \
+    }
+#define UNHANDLED_VERIFY_PREFIX(op_name, ext)                                 \
+  case IREE_VM_OP_CORE_##op_name: {                                           \
+    return iree_vm_check_feature_mismatch(__FILE__, __LINE__, ext,            \
+                                          function_descriptor->requirements); \
+  }
+
+static iree_status_t iree_vm_bytecode_function_verify_bytecode_op(
+    iree_vm_bytecode_module_t* module,
+    iree_vm_bytecode_verify_state_t* verify_state,
+    iree_vm_FunctionSignatureDef_table_t function_signature,
+    iree_vm_FunctionDescriptor_struct_t function_descriptor,
+    iree_const_byte_span_t function_bytecode, uint32_t start_pc,
+    uint32_t max_pc, uint32_t* out_next_pc) {
+  *out_next_pc = 0;
+  uint32_t pc = start_pc;
+  const uint8_t* bytecode_data = function_bytecode.data;
+
+  // NOTE: we keep this as simple as possible so that we can one day auto
+  // generate it from tblgen, which has all the encodings in a similar form
+  // such that we could do string substitution to get the verifier macros.
+
+  // All ops except for Block must be inside of a block. We hoist that check
+  // that here before switching out.
+  IREE_VM_VERIFY_PC_RANGE(pc + 1, max_pc);
+  if (verify_state->in_block == 0) {
+    // If not in a block then the next opcode must be a block.
+    if (bytecode_data[pc] != IREE_VM_OP_CORE_Block) {
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "op at pc %08X is not in a block", pc);
+    }
+  } else {
+    // If in a block then the next opcode must not be a block.
+    if (bytecode_data[pc] == IREE_VM_OP_CORE_Block) {
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "op at pc %08X is a block while still in a block",
+                              pc);
+    }
+  }
+
+  // Get primary opcode. All ops have at least 1 byte.
+  switch (bytecode_data[pc++]) {
+    //===------------------------------------------------------------------===//
+    // Globals
+    //===------------------------------------------------------------------===//
+
+    VERIFY_OP(CORE, GlobalLoadI32, {
+      VM_VerifyGlobalAttr(byte_offset);
+      VM_VerifyRwdataOffset(byte_offset, 4);
+      VM_VerifyResultRegI32(value);
+    });
+
+    VERIFY_OP(CORE, GlobalStoreI32, {
+      VM_VerifyGlobalAttr(byte_offset);
+      VM_VerifyRwdataOffset(byte_offset, 4);
+      VM_VerifyOperandRegI32(value);
+    });
+
+    VERIFY_OP(CORE, GlobalLoadIndirectI32, {
+      VM_VerifyOperandRegI32(byte_offset);
+      // NOTE: we have to verify the offset at runtime.
+      VM_VerifyResultRegI32(value);
+    });
+
+    VERIFY_OP(CORE, GlobalStoreIndirectI32, {
+      VM_VerifyOperandRegI32(byte_offset);
+      // NOTE: we have to verify the offset at runtime.
+      VM_VerifyOperandRegI32(value);
+    });
+
+    VERIFY_OP(CORE, GlobalLoadI64, {
+      VM_VerifyGlobalAttr(byte_offset);
+      VM_VerifyRwdataOffset(byte_offset, 8);
+      VM_VerifyResultRegI64(value);
+    });
+
+    VERIFY_OP(CORE, GlobalStoreI64, {
+      VM_VerifyGlobalAttr(byte_offset);
+      VM_VerifyRwdataOffset(byte_offset, 8);
+      VM_VerifyOperandRegI64(value);
+    });
+
+    VERIFY_OP(CORE, GlobalLoadIndirectI64, {
+      VM_VerifyOperandRegI32(byte_offset);
+      // NOTE: we have to verify the offset at runtime.
+      VM_VerifyResultRegI64(value);
+    });
+
+    VERIFY_OP(CORE, GlobalStoreIndirectI64, {
+      VM_VerifyOperandRegI32(byte_offset);
+      // NOTE: we have to verify the offset at runtime.
+      VM_VerifyOperandRegI64(value);
+    });
+
+    VERIFY_OP(CORE, GlobalLoadRef, {
+      VM_VerifyGlobalAttr(global);
+      VM_VerifyGlobalRefOrdinal(global);
+      VM_VerifyTypeOf(type_def);
+      VM_VerifyResultRegRef(value);
+    });
+
+    VERIFY_OP(CORE, GlobalStoreRef, {
+      VM_VerifyGlobalAttr(global);
+      VM_VerifyGlobalRefOrdinal(global);
+      VM_VerifyTypeOf(type_def);
+      VM_VerifyOperandRegRef(value);
+    });
+
+    VERIFY_OP(CORE, GlobalLoadIndirectRef, {
+      VM_VerifyOperandRegI32(global);
+      // NOTE: we have to verify the ordinal at runtime.
+      VM_VerifyTypeOf(type_def);
+      VM_VerifyResultRegRef(value);
+    });
+
+    VERIFY_OP(CORE, GlobalStoreIndirectRef, {
+      VM_VerifyOperandRegI32(global);
+      // NOTE: we have to verify the ordinal at runtime.
+      VM_VerifyTypeOf(type_def);
+      VM_VerifyOperandRegRef(value);
+    });
+
+    //===------------------------------------------------------------------===//
+    // Constants
+    //===------------------------------------------------------------------===//
+
+    VERIFY_OP(CORE, ConstI32, {
+      VM_VerifyIntAttr32(value);
+      VM_VerifyResultRegI32(result);
+    });
+
+    VERIFY_OP(CORE, ConstI32Zero, { VM_VerifyResultRegI32(result); });
+
+    VERIFY_OP(CORE, ConstI64, {
+      VM_VerifyIntAttr64(value);
+      VM_VerifyResultRegI64(result);
+    });
+
+    VERIFY_OP(CORE, ConstI64Zero, { VM_VerifyResultRegI64(result); });
+
+    VERIFY_OP(CORE, ConstRefZero, { VM_VerifyResultRegRef(result); });
+
+    VERIFY_OP(CORE, ConstRefRodata, {
+      VM_VerifyRodataAttr(rodata);
+      VM_VerifyRodataOrdinal(rodata);
+      VM_VerifyResultRegRef(value);
+    });
+
+    //===------------------------------------------------------------------===//
+    // Buffers
+    //===------------------------------------------------------------------===//
+
+    VERIFY_OP(CORE, BufferAlloc, {
+      VM_VerifyOperandRegI64HostSize(length);
+      VM_VerifyResultRegRef(result);
+    });
+
+    VERIFY_OP(CORE, BufferClone, {
+      VM_VerifyOperandRegRef(source);
+      VM_VerifyOperandRegI64HostSize(offset);
+      VM_VerifyOperandRegI64HostSize(length);
+      VM_VerifyResultRegRef(result);
+    });
+
+    VERIFY_OP(CORE, BufferLength, {
+      VM_VerifyOperandRegRef(buffer);
+      VM_VerifyResultRegI64(result);
+    });
+
+    VERIFY_OP(CORE, BufferCopy, {
+      VM_VerifyOperandRegRef(source_buffer);
+      VM_VerifyOperandRegI64HostSize(source_offset);
+      VM_VerifyOperandRegRef(target_buffer);
+      VM_VerifyOperandRegI64HostSize(target_offset);
+      VM_VerifyOperandRegI64HostSize(length);
+    });
+
+    VERIFY_OP(CORE, BufferCompare, {
+      VM_VerifyOperandRegRef(lhs_buffer);
+      VM_VerifyOperandRegI64HostSize(lhs_offset);
+      VM_VerifyOperandRegRef(rhs_buffer);
+      VM_VerifyOperandRegI64HostSize(rhs_offset);
+      VM_VerifyOperandRegI64HostSize(length);
+      VM_VerifyResultRegI32(result);
+    });
+
+    VERIFY_OP(CORE, BufferFillI8, {
+      VM_VerifyOperandRegRef(target_buffer);
+      VM_VerifyOperandRegI64HostSize(target_offset);
+      VM_VerifyOperandRegI64HostSize(length);
+      VM_VerifyOperandRegI32(value);
+    });
+    VERIFY_OP(CORE, BufferFillI16, {
+      VM_VerifyOperandRegRef(target_buffer);
+      VM_VerifyOperandRegI64HostSize(target_offset);
+      VM_VerifyOperandRegI64HostSize(length);
+      VM_VerifyOperandRegI32(value);
+    });
+    VERIFY_OP(CORE, BufferFillI32, {
+      VM_VerifyOperandRegRef(target_buffer);
+      VM_VerifyOperandRegI64HostSize(target_offset);
+      VM_VerifyOperandRegI64HostSize(length);
+      VM_VerifyOperandRegI32(value);
+    });
+    VERIFY_OP(CORE, BufferFillI64, {
+      VM_VerifyOperandRegRef(target_buffer);
+      VM_VerifyOperandRegI64HostSize(target_offset);
+      VM_VerifyOperandRegI64HostSize(length);
+      VM_VerifyOperandRegI64(value);
+    });
+
+    VERIFY_OP(CORE, BufferLoadI8U, {
+      VM_VerifyOperandRegRef(source_buffer);
+      VM_VerifyOperandRegI64HostSize(source_offset);
+      VM_VerifyResultRegI32(result);
+    });
+    VERIFY_OP(CORE, BufferLoadI8S, {
+      VM_VerifyOperandRegRef(source_buffer);
+      VM_VerifyOperandRegI64HostSize(source_offset);
+      VM_VerifyResultRegI32(result);
+    });
+    VERIFY_OP(CORE, BufferLoadI16U, {
+      VM_VerifyOperandRegRef(source_buffer);
+      VM_VerifyOperandRegI64HostSize(source_offset);
+      VM_VerifyResultRegI32(result);
+    });
+    VERIFY_OP(CORE, BufferLoadI16S, {
+      VM_VerifyOperandRegRef(source_buffer);
+      VM_VerifyOperandRegI64HostSize(source_offset);
+      VM_VerifyResultRegI32(result);
+    });
+    VERIFY_OP(CORE, BufferLoadI32, {
+      VM_VerifyOperandRegRef(source_buffer);
+      VM_VerifyOperandRegI64HostSize(source_offset);
+      VM_VerifyResultRegI32(result);
+    });
+    VERIFY_OP(CORE, BufferLoadI64, {
+      VM_VerifyOperandRegRef(source_buffer);
+      VM_VerifyOperandRegI64HostSize(source_offset);
+      VM_VerifyResultRegI64(result);
+    });
+
+    VERIFY_OP(CORE, BufferStoreI8, {
+      VM_VerifyOperandRegRef(target_buffer);
+      VM_VerifyOperandRegI64HostSize(target_offset);
+      VM_VerifyOperandRegI32(value);
+    });
+    VERIFY_OP(CORE, BufferStoreI16, {
+      VM_VerifyOperandRegRef(target_buffer);
+      VM_VerifyOperandRegI64HostSize(target_offset);
+      VM_VerifyOperandRegI32(value);
+    });
+    VERIFY_OP(CORE, BufferStoreI32, {
+      VM_VerifyOperandRegRef(target_buffer);
+      VM_VerifyOperandRegI64HostSize(target_offset);
+      VM_VerifyOperandRegI32(value);
+    });
+    VERIFY_OP(CORE, BufferStoreI64, {
+      VM_VerifyOperandRegRef(target_buffer);
+      VM_VerifyOperandRegI64HostSize(target_offset);
+      VM_VerifyOperandRegI64(value);
+    });
+
+    //===------------------------------------------------------------------===//
+    // Lists
+    //===------------------------------------------------------------------===//
+
+    VERIFY_OP(CORE, ListAlloc, {
+      VM_VerifyTypeOf(element_type);
+      VM_VerifyOperandRegI32(initial_capacity);
+      VM_VerifyResultRegRef(result);
+    });
+
+    VERIFY_OP(CORE, ListReserve, {
+      VM_VerifyOperandRegRef(list);
+      VM_VerifyOperandRegI32(minimum_capacity);
+    });
+
+    VERIFY_OP(CORE, ListSize, {
+      VM_VerifyOperandRegRef(list);
+      VM_VerifyResultRegI32(result);
+    });
+
+    VERIFY_OP(CORE, ListResize, {
+      VM_VerifyOperandRegRef(list);
+      VM_VerifyOperandRegI32(new_size);
+    });
+
+    VERIFY_OP(CORE, ListGetI32, {
+      VM_VerifyOperandRegRef(list);
+      VM_VerifyOperandRegI32(index);
+      VM_VerifyResultRegI32(result);
+    });
+
+    VERIFY_OP(CORE, ListSetI32, {
+      VM_VerifyOperandRegRef(list);
+      VM_VerifyOperandRegI32(index);
+      VM_VerifyOperandRegI32(raw_value);
+    });
+
+    VERIFY_OP(CORE, ListGetI64, {
+      VM_VerifyOperandRegRef(list);
+      VM_VerifyOperandRegI32(index);
+      VM_VerifyResultRegI64(result);
+    });
+
+    VERIFY_OP(CORE, ListSetI64, {
+      VM_VerifyOperandRegRef(list);
+      VM_VerifyOperandRegI32(index);
+      VM_VerifyOperandRegI64(value);
+    });
+
+    VERIFY_OP(CORE, ListGetRef, {
+      VM_VerifyOperandRegRef(list);
+      VM_VerifyOperandRegI32(index);
+      VM_VerifyTypeOf(type_def);
+      VM_VerifyResultRegRef(result);
+    });
+
+    VERIFY_OP(CORE, ListSetRef, {
+      VM_VerifyOperandRegRef(list);
+      VM_VerifyOperandRegI32(index);
+      VM_VerifyOperandRegRef(value);
+    });
+
+    //===------------------------------------------------------------------===//
+    // Conditional assignment
+    //===------------------------------------------------------------------===//
+
+    VERIFY_OP(CORE, SelectI32, {
+      VM_VerifyOperandRegI32(condition);
+      VM_VerifyOperandRegI32(true_value);
+      VM_VerifyOperandRegI32(false_value);
+      VM_VerifyResultRegI32(result);
+    });
+
+    VERIFY_OP(CORE, SelectI64, {
+      VM_VerifyOperandRegI32(condition);
+      VM_VerifyOperandRegI64(true_value);
+      VM_VerifyOperandRegI64(false_value);
+      VM_VerifyResultRegI64(result);
+    });
+
+    VERIFY_OP(CORE, SelectRef, {
+      VM_VerifyOperandRegI32(condition);
+      // TODO(benvanik): remove the type_id and use either LHS/RHS (if both are
+      // null then output is always null so no need to know the type).
+      VM_VerifyTypeOf(true_value_type_def);
+      VM_VerifyOperandRegRef(true_value);
+      VM_VerifyOperandRegRef(false_value);
+      VM_VerifyResultRegRef(result);
+    });
+
+    VERIFY_OP(CORE, SwitchI32, {
+      VM_VerifyOperandRegI32(index);
+      VM_VerifyIntAttr32(default_value);
+      VM_VerifyVariadicOperandsI32(values);
+      VM_VerifyResultRegI32(result);
+    });
+
+    VERIFY_OP(CORE, SwitchI64, {
+      VM_VerifyOperandRegI32(index);
+      VM_VerifyIntAttr64(default_value);
+      VM_VerifyVariadicOperandsI64(values);
+      VM_VerifyResultRegI64(result);
+    });
+
+    VERIFY_OP(CORE, SwitchRef, {
+      VM_VerifyOperandRegI32(index);
+      VM_VerifyTypeOf(type_def);
+      VM_VerifyOperandRegRef(default_value);
+      VM_VerifyVariadicOperandsRef(values, type_def);
+      VM_VerifyResultRegRef(result);
+    });
+
+    //===------------------------------------------------------------------===//
+    // Native integer arithmetic
+    //===------------------------------------------------------------------===//
+
+    VERIFY_OP_CORE_BINARY_I32(AddI32);
+    VERIFY_OP_CORE_BINARY_I32(SubI32);
+    VERIFY_OP_CORE_BINARY_I32(MulI32);
+    VERIFY_OP_CORE_BINARY_I32(DivI32S);
+    VERIFY_OP_CORE_BINARY_I32(DivI32U);
+    VERIFY_OP_CORE_BINARY_I32(RemI32S);
+    VERIFY_OP_CORE_BINARY_I32(RemI32U);
+    VERIFY_OP_CORE_TERNARY_I32(FMAI32);
+    VERIFY_OP_CORE_UNARY_I32(AbsI32);
+    VERIFY_OP_CORE_UNARY_I32(NotI32);
+    VERIFY_OP_CORE_BINARY_I32(AndI32);
+    VERIFY_OP_CORE_BINARY_I32(OrI32);
+    VERIFY_OP_CORE_BINARY_I32(XorI32);
+    VERIFY_OP_CORE_UNARY_I32(CtlzI32);
+
+    VERIFY_OP_CORE_BINARY_I64(AddI64);
+    VERIFY_OP_CORE_BINARY_I64(SubI64);
+    VERIFY_OP_CORE_BINARY_I64(MulI64);
+    VERIFY_OP_CORE_BINARY_I64(DivI64S);
+    VERIFY_OP_CORE_BINARY_I64(DivI64U);
+    VERIFY_OP_CORE_BINARY_I64(RemI64S);
+    VERIFY_OP_CORE_BINARY_I64(RemI64U);
+    VERIFY_OP_CORE_TERNARY_I64(FMAI64);
+    VERIFY_OP_CORE_UNARY_I64(AbsI64);
+    VERIFY_OP_CORE_UNARY_I64(NotI64);
+    VERIFY_OP_CORE_BINARY_I64(AndI64);
+    VERIFY_OP_CORE_BINARY_I64(OrI64);
+    VERIFY_OP_CORE_BINARY_I64(XorI64);
+    VERIFY_OP_CORE_UNARY_I64(CtlzI64);
+
+    //===------------------------------------------------------------------===//
+    // Casting and type conversion/emulation
+    //===------------------------------------------------------------------===//
+
+    // NOTE: these all operate on 32-bit registers.
+    VERIFY_OP_CORE_UNARY_I32(TruncI32I8);
+    VERIFY_OP_CORE_UNARY_I32(TruncI32I16);
+    VERIFY_OP_CORE_UNARY_I32(ExtI8I32S);
+    VERIFY_OP_CORE_UNARY_I32(ExtI8I32U);
+    VERIFY_OP_CORE_UNARY_I32(ExtI16I32S);
+    VERIFY_OP_CORE_UNARY_I32(ExtI16I32U);
+
+    // NOTE: 64-bit ones are actually changing register widths.
+    VERIFY_OP(CORE, TruncI64I32, {
+      VM_VerifyOperandRegI64(operand);
+      VM_VerifyResultRegI32(result);
+    });
+    VERIFY_OP(CORE, ExtI32I64S, {
+      VM_VerifyOperandRegI32(operand);
+      VM_VerifyResultRegI64(result);
+    });
+    VERIFY_OP(CORE, ExtI32I64U, {
+      VM_VerifyOperandRegI32(operand);
+      VM_VerifyResultRegI64(result);
+    });
+
+    //===------------------------------------------------------------------===//
+    // Native bitwise shifts and rotates
+    //===------------------------------------------------------------------===//
+
+#define VERIFY_OP_CORE_SHIFT_I32(op_name) \
+  VERIFY_OP(CORE, op_name, {              \
+    VM_VerifyOperandRegI32(operand);      \
+    VM_VerifyOperandRegI32(amount);       \
+    VM_VerifyResultRegI32(result);        \
+  });
+
+    VERIFY_OP_CORE_SHIFT_I32(ShlI32);
+    VERIFY_OP_CORE_SHIFT_I32(ShrI32S);
+    VERIFY_OP_CORE_SHIFT_I32(ShrI32U);
+
+#define VERIFY_OP_CORE_SHIFT_I64(op_name) \
+  VERIFY_OP(CORE, op_name, {              \
+    VM_VerifyOperandRegI64(operand);      \
+    VM_VerifyOperandRegI32(amount);       \
+    VM_VerifyResultRegI64(result);        \
+  });
+
+    VERIFY_OP_CORE_SHIFT_I64(ShlI64);
+    VERIFY_OP_CORE_SHIFT_I64(ShrI64S);
+    VERIFY_OP_CORE_SHIFT_I64(ShrI64U);
+
+    //===------------------------------------------------------------------===//
+    // Comparison ops
+    //===------------------------------------------------------------------===//
+
+    VERIFY_OP_CORE_BINARY_I32(CmpEQI32);
+    VERIFY_OP_CORE_BINARY_I32(CmpNEI32);
+    VERIFY_OP_CORE_BINARY_I32(CmpLTI32S);
+    VERIFY_OP_CORE_BINARY_I32(CmpLTI32U);
+    VERIFY_OP_CORE_UNARY_I32(CmpNZI32);
+
+#define VERIFY_OP_CORE_CMP_I64(op_name) \
+  VERIFY_OP(CORE, op_name, {            \
+    VM_VerifyOperandRegI64(lhs);        \
+    VM_VerifyOperandRegI64(rhs);        \
+    VM_VerifyResultRegI32(result);      \
+  });
+
+    VERIFY_OP_CORE_CMP_I64(CmpEQI64);
+    VERIFY_OP_CORE_CMP_I64(CmpNEI64);
+    VERIFY_OP_CORE_CMP_I64(CmpLTI64S);
+    VERIFY_OP_CORE_CMP_I64(CmpLTI64U);
+    VERIFY_OP(CORE, CmpNZI64, {
+      VM_VerifyOperandRegI64(operand);
+      VM_VerifyResultRegI32(result);
+    });
+
+    VERIFY_OP(CORE, CmpEQRef, {
+      VM_VerifyOperandRegRef(lhs);
+      VM_VerifyOperandRegRef(rhs);
+      VM_VerifyResultRegI32(result);
+    });
+    VERIFY_OP(CORE, CmpNERef, {
+      VM_VerifyOperandRegRef(lhs);
+      VM_VerifyOperandRegRef(rhs);
+      VM_VerifyResultRegI32(result);
+    });
+    VERIFY_OP(CORE, CmpNZRef, {
+      VM_VerifyOperandRegRef(operand);
+      VM_VerifyResultRegI32(result);
+    });
+
+    //===------------------------------------------------------------------===//
+    // Control flow
+    //===------------------------------------------------------------------===//
+
+    VERIFY_OP(CORE, Block, {
+      // Define the new block in the block list. It may already be declared from
+      // a prior branch.
+      iree_vm_bytecode_block_t* block = NULL;
+      IREE_RETURN_IF_ERROR(iree_vm_bytecode_block_list_insert(
+          &verify_state->block_list, pc - 1, &block));
+      block->defined = 1;
+      verify_state->in_block = 1;
+    });
+
+    VERIFY_OP(CORE, Branch, {
+      VM_VerifyBranchTarget(dest_pc);
+      VM_VerifyBranchOperands(operands);
+      verify_state->in_block = 0;  // terminator
+    });
+
+    VERIFY_OP(CORE, CondBranch, {
+      VM_VerifyOperandRegI32(condition);
+      VM_VerifyBranchTarget(true_dest_pc);
+      VM_VerifyBranchOperands(true_operands);
+      VM_VerifyBranchTarget(false_dest_pc);
+      VM_VerifyBranchOperands(false_operands);
+      verify_state->in_block = 0;  // terminator
+    });
+
+    VERIFY_OP(CORE, Call, {
+      VM_VerifyFuncAttr(callee_ordinal);
+      VM_VerifyVariadicOperandsAny(operands);
+      VM_VerifyVariadicResultsAny(results);
+      if (VM_IsImportOrdinal(callee_ordinal)) {
+        VM_UnmaskImportOrdinal(callee_ordinal);
+        VM_VerifyImportOrdinal(callee_ordinal);
+        iree_vm_ImportFunctionDef_table_t import_def =
+            iree_vm_ImportFunctionDef_vec_at(verify_state->imported_functions,
+                                             callee_ordinal);
+        IREE_RETURN_IF_ERROR(
+            iree_vm_bytecode_function_verify_call(
+                verify_state, iree_vm_ImportFunctionDef_signature(import_def),
+                /*segment_sizes=*/NULL, operands, results),
+            "call to import '%s'",
+            iree_vm_ImportFunctionDef_full_name(import_def));
+      } else {
+        VM_VerifyFunctionOrdinal(callee_ordinal);
+        IREE_RETURN_IF_ERROR(
+            iree_vm_bytecode_function_verify_call(
+                verify_state,
+                iree_vm_FunctionSignatureDef_vec_at(
+                    verify_state->function_signatures, callee_ordinal),
+                /*segment_sizes=*/NULL, operands, results),
+            "call to internal function %d", callee_ordinal);
+      }
+    });
+
+    VERIFY_OP(CORE, CallVariadic, {
+      VM_VerifyFuncAttr(callee_ordinal);
+      VM_VerifyVariadicOperands(segment_sizes);
+      VM_VerifyVariadicOperandsAny(operands);
+      VM_VerifyVariadicResultsAny(results);
+      if (IREE_UNLIKELY(!VM_IsImportOrdinal(callee_ordinal))) {
+        // Variadic calls are currently only supported for import functions.
+        return iree_make_status(
+            IREE_STATUS_FAILED_PRECONDITION,
+            "variadic calls only supported for internal callees");
+      }
+      VM_UnmaskImportOrdinal(callee_ordinal);
+      VM_VerifyImportOrdinal(callee_ordinal);
+      iree_vm_ImportFunctionDef_table_t import_def =
+          iree_vm_ImportFunctionDef_vec_at(verify_state->imported_functions,
+                                           callee_ordinal);
+      IREE_RETURN_IF_ERROR(
+          iree_vm_bytecode_function_verify_call(
+              verify_state, iree_vm_ImportFunctionDef_signature(import_def),
+              segment_sizes, operands, results),
+          "variadic call to import '%s'",
+          iree_vm_ImportFunctionDef_full_name(import_def));
+    });
+
+    VERIFY_OP(CORE, Return, {
+      VM_VerifyVariadicOperandsAny(operands);
+      IREE_RETURN_IF_ERROR(iree_vm_bytecode_function_verify_cconv_registers(
+          verify_state, verify_state->cconv_results, /*segment_sizes=*/NULL,
+          operands));
+      verify_state->in_block = 0;  // terminator
+    });
+
+    VERIFY_OP(CORE, Fail, {
+      VM_VerifyOperandRegI32(status);
+      iree_string_view_t message;
+      VM_VerifyStrAttr(message, &message);
+      verify_state->in_block = 0;  // terminator
+    });
+
+    VERIFY_OP(CORE, ImportResolved, {
+      VM_VerifyFuncAttr(import_ordinal);
+      if (IREE_UNLIKELY(!VM_IsImportOrdinal(import_ordinal))) {
+        return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                                "function ordinal %u is not an import ordinal",
+                                import_ordinal);
+      }
+      VM_UnmaskImportOrdinal(import_ordinal);
+      VM_VerifyImportOrdinal(import_ordinal);
+      VM_VerifyResultRegI32(result);
+    });
+
+    //===------------------------------------------------------------------===//
+    // Async/fiber ops
+    //===------------------------------------------------------------------===//
+
+    VERIFY_OP(CORE, Yield, {
+      VM_VerifyBranchTarget(dest_pc);
+      VM_VerifyBranchOperands(operands);
+      verify_state->in_block = 0;  // terminator
+    });
+
+    //===------------------------------------------------------------------===//
+    // Debugging
+    //===------------------------------------------------------------------===//
+
+    VERIFY_OP(CORE, Trace, {
+      iree_string_view_t event_name;
+      VM_VerifyStrAttr(event_name, &event_name);
+      VM_VerifyVariadicOperandsAny(operands);
+    });
+
+    VERIFY_OP(CORE, Print, {
+      iree_string_view_t event_name;
+      VM_VerifyStrAttr(event_name, &event_name);
+      VM_VerifyVariadicOperandsAny(operands);
+    });
+
+    VERIFY_OP(CORE, Break, {
+      VM_VerifyBranchTarget(dest_pc);
+      VM_VerifyBranchOperands(operands);
+      verify_state->in_block = 0;  // terminator
+    });
+
+    VERIFY_OP(CORE, CondBreak, {
+      VM_VerifyOperandRegI32(condition);
+      VM_VerifyBranchTarget(dest);
+      VM_VerifyBranchOperands(operands);
+      verify_state->in_block = 0;  // terminator
+    });
+
+    //===------------------------------------------------------------------===//
+    // Extension trampolines
+    //===------------------------------------------------------------------===//
+
+#if IREE_VM_EXT_F32_ENABLE
+    BEGIN_VERIFY_PREFIX(PrefixExtF32, iree_vm_FeatureBits_EXT_F32)
+
+    //===----------------------------------------------------------------===//
+    // ExtF32: Globals
+    //===----------------------------------------------------------------===//
+
+    VERIFY_OP(EXT_F32, GlobalLoadF32, {
+      VM_VerifyGlobalAttr(byte_offset);
+      VM_VerifyRwdataOffset(byte_offset, 4);
+      VM_VerifyResultRegF32(value);
+    });
+
+    VERIFY_OP(EXT_F32, GlobalStoreF32, {
+      VM_VerifyGlobalAttr(byte_offset);
+      VM_VerifyRwdataOffset(byte_offset, 4);
+      VM_VerifyOperandRegF32(value);
+    });
+
+    VERIFY_OP(EXT_F32, GlobalLoadIndirectF32, {
+      VM_VerifyOperandRegI32(byte_offset);
+      // NOTE: we have to verify the offset at runtime.
+      VM_VerifyResultRegF32(value);
+    });
+
+    VERIFY_OP(EXT_F32, GlobalStoreIndirectF32, {
+      VM_VerifyOperandRegI32(byte_offset);
+      // NOTE: we have to verify the offset at runtime.
+      VM_VerifyOperandRegF32(value);
+    });
+
+    //===----------------------------------------------------------------===//
+    // ExtF32: Constants
+    //===----------------------------------------------------------------===//
+
+    VERIFY_OP(EXT_F32, ConstF32, {
+      VM_VerifyFloatAttr32(value);
+      VM_VerifyResultRegF32(result);
+    });
+
+    VERIFY_OP(EXT_F32, ConstF32Zero, { VM_VerifyResultRegF32(result); });
+
+    //===----------------------------------------------------------------===//
+    // ExtF32: Lists
+    //===----------------------------------------------------------------===//
+
+    VERIFY_OP(EXT_F32, ListGetF32, {
+      VM_VerifyOperandRegRef(list);
+      VM_VerifyOperandRegI32(index);
+      VM_VerifyResultRegF32(result);
+    });
+
+    VERIFY_OP(EXT_F32, ListSetF32, {
+      VM_VerifyOperandRegRef(list);
+      VM_VerifyOperandRegI32(index);
+      VM_VerifyOperandRegF32(value);
+    });
+
+    //===----------------------------------------------------------------===//
+    // ExtF32: Conditional assignment
+    //===----------------------------------------------------------------===//
+
+    VERIFY_OP(EXT_F32, SelectF32, {
+      VM_VerifyOperandRegI32(condition);
+      VM_VerifyOperandRegF32(true_value);
+      VM_VerifyOperandRegF32(false_value);
+      VM_VerifyResultRegF32(result);
+    });
+
+    VERIFY_OP(EXT_F32, SwitchF32, {
+      VM_VerifyOperandRegI32(index);
+      VM_VerifyFloatAttr32(default_value);
+      VM_VerifyVariadicOperandsF32(values);
+      VM_VerifyResultRegF32(result);
+    });
+
+    //===----------------------------------------------------------------===//
+    // ExtF32: Native floating-point arithmetic
+    //===----------------------------------------------------------------===//
+
+    VERIFY_OP_EXT_F32_BINARY_F32(AddF32);
+    VERIFY_OP_EXT_F32_BINARY_F32(SubF32);
+    VERIFY_OP_EXT_F32_BINARY_F32(MulF32);
+    VERIFY_OP_EXT_F32_BINARY_F32(DivF32);
+    VERIFY_OP_EXT_F32_BINARY_F32(RemF32);
+    VERIFY_OP_EXT_F32_TERNARY_F32(FMAF32);
+    VERIFY_OP_EXT_F32_UNARY_F32(AbsF32);
+    VERIFY_OP_EXT_F32_UNARY_F32(NegF32);
+    VERIFY_OP_EXT_F32_UNARY_F32(CeilF32);
+    VERIFY_OP_EXT_F32_UNARY_F32(FloorF32);
+    VERIFY_OP_EXT_F32_UNARY_F32(RoundF32);
+
+    VERIFY_OP_EXT_F32_UNARY_F32(AtanF32);
+    VERIFY_OP_EXT_F32_BINARY_F32(Atan2F32);
+    VERIFY_OP_EXT_F32_UNARY_F32(CosF32);
+    VERIFY_OP_EXT_F32_UNARY_F32(SinF32);
+    VERIFY_OP_EXT_F32_UNARY_F32(ExpF32);
+    VERIFY_OP_EXT_F32_UNARY_F32(Exp2F32);
+    VERIFY_OP_EXT_F32_UNARY_F32(ExpM1F32);
+    VERIFY_OP_EXT_F32_UNARY_F32(LogF32);
+    VERIFY_OP_EXT_F32_UNARY_F32(Log10F32);
+    VERIFY_OP_EXT_F32_UNARY_F32(Log1pF32);
+    VERIFY_OP_EXT_F32_UNARY_F32(Log2F32);
+    VERIFY_OP_EXT_F32_BINARY_F32(PowF32);
+    VERIFY_OP_EXT_F32_UNARY_F32(RsqrtF32);
+    VERIFY_OP_EXT_F32_UNARY_F32(SqrtF32);
+    VERIFY_OP_EXT_F32_UNARY_F32(TanhF32);
+    VERIFY_OP_EXT_F32_UNARY_F32(ErfF32);
+
+    //===----------------------------------------------------------------===//
+    // ExtF32: Casting and type conversion/emulation
+    //===----------------------------------------------------------------===//
+
+    VERIFY_OP(EXT_F32, CastSI32F32, {
+      VM_VerifyOperandRegI32(operand);
+      VM_VerifyResultRegF32(result);
+    });
+    VERIFY_OP(EXT_F32, CastUI32F32, {
+      VM_VerifyOperandRegI32(operand);
+      VM_VerifyResultRegF32(result);
+    });
+    VERIFY_OP(EXT_F32, CastF32SI32, {
+      VM_VerifyOperandRegF32(operand);
+      VM_VerifyResultRegI32(result);
+    });
+    VERIFY_OP(EXT_F32, CastF32UI32, {
+      VM_VerifyOperandRegF32(operand);
+      VM_VerifyResultRegI32(result);
+    });
+    VERIFY_OP(EXT_F32, BitcastI32F32, {
+      VM_VerifyOperandRegI32(operand);
+      VM_VerifyResultRegF32(result);
+    });
+    VERIFY_OP(EXT_F32, BitcastF32I32, {
+      VM_VerifyOperandRegF32(operand);
+      VM_VerifyResultRegI32(result);
+    });
+
+    //===----------------------------------------------------------------===//
+    // ExtF32: Comparison ops
+    //===----------------------------------------------------------------===//
+
+#define VERIFY_OP_EXT_F32_CMP_F32(op_name) \
+  VERIFY_OP(EXT_F32, op_name, {            \
+    VM_VerifyOperandRegF32(lhs);           \
+    VM_VerifyOperandRegF32(rhs);           \
+    VM_VerifyResultRegI32(result);         \
+  });
+
+    VERIFY_OP_EXT_F32_CMP_F32(CmpEQF32O);
+    VERIFY_OP_EXT_F32_CMP_F32(CmpEQF32U);
+    VERIFY_OP_EXT_F32_CMP_F32(CmpNEF32O);
+    VERIFY_OP_EXT_F32_CMP_F32(CmpNEF32U);
+    VERIFY_OP_EXT_F32_CMP_F32(CmpLTF32O);
+    VERIFY_OP_EXT_F32_CMP_F32(CmpLTF32U);
+    VERIFY_OP_EXT_F32_CMP_F32(CmpLTEF32O);
+    VERIFY_OP_EXT_F32_CMP_F32(CmpLTEF32U);
+    VERIFY_OP(EXT_F32, CmpNaNF32, {
+      VM_VerifyOperandRegF32(operand);
+      VM_VerifyResultRegI32(result);
+    });
+
+    //===----------------------------------------------------------------===//
+    // ExtF32: Buffers
+    //===----------------------------------------------------------------===//
+
+    VERIFY_OP(EXT_F32, BufferFillF32, {
+      VM_VerifyOperandRegRef(target_buffer);
+      VM_VerifyOperandRegI64HostSize(target_offset);
+      VM_VerifyOperandRegI64HostSize(length);
+      VM_VerifyOperandRegF32(value);
+    });
+
+    VERIFY_OP(EXT_F32, BufferLoadF32, {
+      VM_VerifyOperandRegRef(source_buffer);
+      VM_VerifyOperandRegI64HostSize(source_offset);
+      VM_VerifyResultRegF32(result);
+    });
+
+    VERIFY_OP(EXT_F32, BufferStoreF32, {
+      VM_VerifyOperandRegRef(target_buffer);
+      VM_VerifyOperandRegI64HostSize(target_offset);
+      VM_VerifyOperandRegF32(value);
+    });
+
+    END_VERIFY_PREFIX();
+#else
+    UNHANDLED_VERIFY_PREFIX(PrefixExtF32, iree_vm_FeatureBits_EXT_F32);
+#endif  // IREE_VM_EXT_F32_ENABLE
+
+    VERIFY_OP(CORE, PrefixExtF64, {
+      IREE_VM_VERIFY_REQUIREMENT(iree_vm_FeatureBits_EXT_F64);
+      return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
+                              "EXT_64 not yet implemented");
+    });
+
+    default:
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "unrecognized opcode %u", bytecode_data[pc - 1]);
+  }
+
+  *out_next_pc = pc;
+  return iree_ok_status();
+}
diff --git a/runtime/src/iree/vm/bytecode/verifier.h b/runtime/src/iree/vm/bytecode/verifier.h
new file mode 100644
index 0000000..ae7f1af
--- /dev/null
+++ b/runtime/src/iree/vm/bytecode/verifier.h
@@ -0,0 +1,38 @@
+// Copyright 2023 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef IREE_VM_BYTECODE_VERIFIER_H_
+#define IREE_VM_BYTECODE_VERIFIER_H_
+
+#include "iree/base/api.h"
+#include "iree/vm/api.h"
+#include "iree/vm/bytecode/module_impl.h"
+
+// Verifies the structure of the FlatBuffer so that we can avoid doing so during
+// runtime. There are still some conditions we must be aware of (such as omitted
+// names on functions with internal linkage), however we shouldn't need to
+// bounds check anything within the FlatBuffer after this succeeds.
+iree_status_t iree_vm_bytecode_module_flatbuffer_verify(
+    iree_const_byte_span_t archive_contents,
+    iree_const_byte_span_t flatbuffer_contents,
+    iree_host_size_t archive_rodata_offset);
+
+// Verifies the bytecode contained within the given |function_ordinal|.
+// Assumes that all information on |module| has been verified and only function
+// information requires verification.
+//
+// NOTE: verification only checks that the function is well-formed and not that
+// it is correct or will execute successfully. The only thing this tries to
+// guarantee is that executing the bytecode won't cause a crash.
+//
+// If verification requires transient allocations for tracking they will be made
+// from |scratch_allocator|. No allocation will live outside of the function and
+// callers may provide stack-based arenas.
+iree_status_t iree_vm_bytecode_function_verify(
+    iree_vm_bytecode_module_t* module, uint16_t function_ordinal,
+    iree_allocator_t scratch_allocator);
+
+#endif  // IREE_VM_BYTECODE_VERIFIER_H_
diff --git a/runtime/src/iree/vm/list.c b/runtime/src/iree/vm/list.c
index d7bdef5..0dafa55 100644
--- a/runtime/src/iree/vm/list.c
+++ b/runtime/src/iree/vm/list.c
@@ -62,8 +62,6 @@
   void* storage;
 };
 
-static iree_vm_ref_type_descriptor_t iree_vm_list_descriptor = {0};
-
 IREE_VM_DEFINE_TYPE_ADAPTERS(iree_vm_list, iree_vm_list_t);
 
 static void iree_vm_list_retain_range(iree_vm_list_t* list,
diff --git a/runtime/src/iree/vm/list_test.cc b/runtime/src/iree/vm/list_test.cc
index e9b1d08..f20bc06 100644
--- a/runtime/src/iree/vm/list_test.cc
+++ b/runtime/src/iree/vm/list_test.cc
@@ -23,7 +23,6 @@
  private:
   float data_ = 1.0f;
 };
-static iree_vm_ref_type_descriptor_t test_a_descriptor = {0};
 IREE_VM_DECLARE_TYPE_ADAPTERS(test_a, A);
 IREE_VM_DEFINE_TYPE_ADAPTERS(test_a, A);
 
@@ -35,7 +34,6 @@
  private:
   int data_ = 2;
 };
-static iree_vm_ref_type_descriptor_t test_b_descriptor = {0};
 IREE_VM_DECLARE_TYPE_ADAPTERS(test_b, B);
 IREE_VM_DEFINE_TYPE_ADAPTERS(test_b, B);
 
diff --git a/runtime/src/iree/vm/module.c b/runtime/src/iree/vm/module.c
index 30b0619..0be0bfa 100644
--- a/runtime/src/iree/vm/module.c
+++ b/runtime/src/iree/vm/module.c
@@ -70,14 +70,14 @@
               break;
             default:
               return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
-                                      "unsupported cconv span type %c",
+                                      "unsupported cconv span type '%c'",
                                       cconv_fragment.data[i]);
           }
         }
       } break;
       default:
         return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
-                                "unsupported cconv type %c",
+                                "unsupported cconv type '%c'",
                                 cconv_fragment.data[i]);
     }
   }
@@ -159,7 +159,7 @@
               break;
             default:
               return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
-                                      "unsupported cconv span type %c",
+                                      "unsupported cconv span type '%c'",
                                       cconv_fragment.data[i]);
           }
         }
@@ -167,7 +167,7 @@
       } break;
       default:
         return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
-                                "unsupported cconv type %c",
+                                "unsupported cconv type '%c'",
                                 cconv_fragment.data[i]);
     }
   }
@@ -317,6 +317,7 @@
 IREE_API_EXPORT iree_string_view_t
 iree_vm_function_name(const iree_vm_function_t* function) {
   IREE_ASSERT_ARGUMENT(function);
+  if (!function->module) return iree_string_view_empty();
   iree_string_view_t name;
   iree_status_t status = function->module->get_function(
       function->module->self, function->linkage, function->ordinal,
diff --git a/runtime/src/iree/vm/ops.h b/runtime/src/iree/vm/ops.h
index 9728330..1bfffbd 100644
--- a/runtime/src/iree/vm/ops.h
+++ b/runtime/src/iree/vm/ops.h
@@ -59,118 +59,169 @@
   return iree_ok_status();
 }
 
+#define vm_buffer_fill_inline(buffer, element_offset, element_length, \
+                              element_type, value)                    \
+  element_type* IREE_RESTRICT buffer_ptr = NULL;                      \
+  iree_vm_buffer_check_rw(buffer, element_offset, element_length,     \
+                          element_type, buffer_ptr);                  \
+  for (iree_host_size_t i = 0; i < element_length; ++i) {             \
+    buffer_ptr[i] = value;                                            \
+  }
+
+#define vm_buffer_fill_i8_inline(buffer, element_offset, element_length, \
+                                 value)                                  \
+  uint8_t* IREE_RESTRICT buffer_ptr = NULL;                              \
+  iree_vm_buffer_check_rw(buffer, offset, length, uint8_t, buffer_ptr);  \
+  memset(buffer_ptr, value, length);
 static inline iree_status_t vm_buffer_fill_i8(iree_vm_buffer_t* buffer,
                                               iree_host_size_t offset,
                                               iree_host_size_t length,
                                               uint8_t value) {
-  return iree_vm_buffer_fill_elements(buffer, offset, length / sizeof(value),
-                                      sizeof(value), &value);
+  vm_buffer_fill_i8_inline(buffer, offset, length, value);
+  return iree_ok_status();
 }
 
+#define vm_buffer_fill_i16_inline(buffer, element_offset, element_length, \
+                                  value)                                  \
+  vm_buffer_fill_inline(buffer, element_offset, element_length, uint16_t, value)
 static inline iree_status_t vm_buffer_fill_i16(iree_vm_buffer_t* buffer,
                                                iree_host_size_t offset,
                                                iree_host_size_t length,
                                                uint16_t value) {
-  return iree_vm_buffer_fill_elements(buffer, offset, length / sizeof(value),
-                                      sizeof(value), &value);
+  vm_buffer_fill_i16_inline(buffer, offset, length, value);
+  return iree_ok_status();
 }
 
+#define vm_buffer_fill_i32_inline(buffer, element_offset, element_length, \
+                                  value)                                  \
+  vm_buffer_fill_inline(buffer, element_offset, element_length, uint32_t, value)
 static inline iree_status_t vm_buffer_fill_i32(iree_vm_buffer_t* buffer,
                                                iree_host_size_t offset,
                                                iree_host_size_t length,
                                                uint32_t value) {
-  return iree_vm_buffer_fill_elements(buffer, offset, length / sizeof(value),
-                                      sizeof(value), &value);
+  vm_buffer_fill_i32_inline(buffer, offset, length, value);
+  return iree_ok_status();
 }
 
+#define vm_buffer_fill_i64_inline(buffer, element_offset, element_length, \
+                                  value)                                  \
+  vm_buffer_fill_inline(buffer, element_offset, element_length, uint64_t, value)
 static inline iree_status_t vm_buffer_fill_i64(iree_vm_buffer_t* buffer,
                                                iree_host_size_t offset,
                                                iree_host_size_t length,
                                                uint64_t value) {
-  return iree_vm_buffer_fill_elements(buffer, offset, length / sizeof(value),
-                                      sizeof(value), &value);
+  vm_buffer_fill_i64_inline(buffer, offset, length, value);
+  return iree_ok_status();
 }
 
+#define vm_buffer_load_i8u_inline(buffer, element_offset, result)          \
+  const uint8_t* IREE_RESTRICT buffer_ptr = NULL;                          \
+  iree_vm_buffer_check_ro(buffer, element_offset, 1, uint8_t, buffer_ptr); \
+  *result = vm_ext_i8i32u(*buffer_ptr);
 static inline iree_status_t vm_buffer_load_i8u(iree_vm_buffer_t* buffer,
                                                iree_host_size_t offset,
                                                int32_t* result) {
-  uint8_t result_x8 = 0;
-  IREE_RETURN_IF_ERROR(iree_vm_buffer_read_elements(buffer, offset, &result_x8,
-                                                    1, sizeof(result_x8)));
-  *result = vm_ext_i8i32u(result_x8);
+  vm_buffer_load_i8u_inline(buffer, offset, result);
   return iree_ok_status();
 }
 
+#define vm_buffer_load_i8s_inline(buffer, element_offset, result)         \
+  const int8_t* IREE_RESTRICT buffer_ptr = NULL;                          \
+  iree_vm_buffer_check_ro(buffer, element_offset, 1, int8_t, buffer_ptr); \
+  *result = vm_ext_i8i32s(*buffer_ptr);
 static inline iree_status_t vm_buffer_load_i8s(iree_vm_buffer_t* buffer,
                                                iree_host_size_t offset,
                                                int32_t* result) {
-  int8_t result_x8 = 0;
-  IREE_RETURN_IF_ERROR(iree_vm_buffer_read_elements(buffer, offset, &result_x8,
-                                                    1, sizeof(result_x8)));
-  *result = vm_ext_i8i32s(result_x8);
+  vm_buffer_load_i8s_inline(buffer, offset, result);
   return iree_ok_status();
 }
 
+#define vm_buffer_load_i16u_inline(buffer, element_offset, result)         \
+  const int16_t* IREE_RESTRICT buffer_ptr = NULL;                          \
+  iree_vm_buffer_check_ro(buffer, element_offset, 1, int16_t, buffer_ptr); \
+  *result = vm_ext_i16i32u(*buffer_ptr);
 static inline iree_status_t vm_buffer_load_i16u(iree_vm_buffer_t* buffer,
                                                 iree_host_size_t offset,
                                                 int32_t* result) {
-  uint16_t result_x16 = 0;
-  IREE_RETURN_IF_ERROR(iree_vm_buffer_read_elements(
-      buffer, offset * sizeof(result_x16), &result_x16, 1, sizeof(result_x16)));
-  *result = vm_ext_i16i32u(result_x16);
+  vm_buffer_load_i16u_inline(buffer, offset, result);
   return iree_ok_status();
 }
 
+#define vm_buffer_load_i16s_inline(buffer, element_offset, result)         \
+  const int16_t* IREE_RESTRICT buffer_ptr = NULL;                          \
+  iree_vm_buffer_check_ro(buffer, element_offset, 1, int16_t, buffer_ptr); \
+  *result = vm_ext_i16i32s(*buffer_ptr);
 static inline iree_status_t vm_buffer_load_i16s(iree_vm_buffer_t* buffer,
                                                 iree_host_size_t offset,
                                                 int32_t* result) {
-  int16_t result_x16 = 0;
-  IREE_RETURN_IF_ERROR(iree_vm_buffer_read_elements(
-      buffer, offset * sizeof(result_x16), &result_x16, 1, sizeof(result_x16)));
-  *result = vm_ext_i16i32s(result_x16);
+  vm_buffer_load_i16s_inline(buffer, offset, result);
   return iree_ok_status();
 }
 
+#define vm_buffer_load_i32_inline(buffer, element_offset, result)          \
+  const int32_t* IREE_RESTRICT buffer_ptr = NULL;                          \
+  iree_vm_buffer_check_ro(buffer, element_offset, 1, int32_t, buffer_ptr); \
+  *result = *buffer_ptr;
 static inline iree_status_t vm_buffer_load_i32(iree_vm_buffer_t* buffer,
                                                iree_host_size_t offset,
                                                int32_t* result) {
-  return iree_vm_buffer_read_elements(buffer, offset * sizeof(*result), result,
-                                      1, sizeof(*result));
+  vm_buffer_load_i32_inline(buffer, offset, result);
+  return iree_ok_status();
 }
 
+#define vm_buffer_load_i64_inline(buffer, element_offset, result)          \
+  const int64_t* IREE_RESTRICT buffer_ptr = NULL;                          \
+  iree_vm_buffer_check_ro(buffer, element_offset, 1, int64_t, buffer_ptr); \
+  *result = *buffer_ptr;
 static inline iree_status_t vm_buffer_load_i64(iree_vm_buffer_t* buffer,
                                                iree_host_size_t offset,
                                                int64_t* result) {
-  return iree_vm_buffer_read_elements(buffer, offset * sizeof(*result), result,
-                                      1, sizeof(*result));
+  vm_buffer_load_i64_inline(buffer, offset, result);
+  return iree_ok_status();
 }
 
+#define vm_buffer_store_i8_inline(buffer, element_offset, value)           \
+  uint8_t* IREE_RESTRICT buffer_ptr = NULL;                                \
+  iree_vm_buffer_check_rw(buffer, element_offset, 1, uint8_t, buffer_ptr); \
+  *buffer_ptr = value;
 static inline iree_status_t vm_buffer_store_i8(iree_vm_buffer_t* buffer,
                                                iree_host_size_t offset,
                                                uint8_t value) {
-  return iree_vm_buffer_write_elements(&value, buffer, offset * sizeof(value),
-                                       1, sizeof(value));
+  vm_buffer_store_i8_inline(buffer, offset, value);
+  return iree_ok_status();
 }
 
+#define vm_buffer_store_i16_inline(buffer, element_offset, value)           \
+  uint16_t* IREE_RESTRICT buffer_ptr = NULL;                                \
+  iree_vm_buffer_check_rw(buffer, element_offset, 1, uint16_t, buffer_ptr); \
+  *buffer_ptr = value;
 static inline iree_status_t vm_buffer_store_i16(iree_vm_buffer_t* buffer,
                                                 iree_host_size_t offset,
                                                 uint16_t value) {
-  return iree_vm_buffer_write_elements(&value, buffer, offset * sizeof(value),
-                                       1, sizeof(value));
+  vm_buffer_store_i16_inline(buffer, offset, value);
+  return iree_ok_status();
 }
 
+#define vm_buffer_store_i32_inline(buffer, element_offset, value)           \
+  uint32_t* IREE_RESTRICT buffer_ptr = NULL;                                \
+  iree_vm_buffer_check_rw(buffer, element_offset, 1, uint32_t, buffer_ptr); \
+  *buffer_ptr = value;
 static inline iree_status_t vm_buffer_store_i32(iree_vm_buffer_t* buffer,
                                                 iree_host_size_t offset,
                                                 uint32_t value) {
-  return iree_vm_buffer_write_elements(&value, buffer, offset * sizeof(value),
-                                       1, sizeof(value));
+  vm_buffer_store_i32_inline(buffer, offset, value);
+  return iree_ok_status();
 }
 
+#define vm_buffer_store_i64_inline(buffer, element_offset, value)           \
+  uint64_t* IREE_RESTRICT buffer_ptr = NULL;                                \
+  iree_vm_buffer_check_rw(buffer, element_offset, 1, uint64_t, buffer_ptr); \
+  *buffer_ptr = value;
 static inline iree_status_t vm_buffer_store_i64(iree_vm_buffer_t* buffer,
                                                 iree_host_size_t offset,
                                                 uint64_t value) {
-  return iree_vm_buffer_write_elements(&value, buffer, offset * sizeof(value),
-                                       1, sizeof(value));
+  vm_buffer_store_i64_inline(buffer, offset, value);
+  return iree_ok_status();
 }
 
 //===------------------------------------------------------------------===//
@@ -411,26 +462,37 @@
 // ExtF32: Buffers
 //===------------------------------------------------------------------===//
 
+#define vm_buffer_fill_f32_inline(buffer, element_offset, element_length, \
+                                  value)                                  \
+  vm_buffer_fill_inline(buffer, element_offset, element_length, float, value)
 static inline iree_status_t vm_buffer_fill_f32(iree_vm_buffer_t* buffer,
                                                iree_host_size_t offset,
                                                iree_host_size_t length,
                                                float value) {
-  return iree_vm_buffer_fill_elements(buffer, offset, length / sizeof(value),
-                                      sizeof(value), &value);
+  vm_buffer_fill_f32_inline(buffer, offset, length, value);
+  return iree_ok_status();
 }
 
+#define vm_buffer_load_f32_inline(buffer, element_offset, result)        \
+  const float* IREE_RESTRICT buffer_ptr = NULL;                          \
+  iree_vm_buffer_check_ro(buffer, element_offset, 1, float, buffer_ptr); \
+  *result = *buffer_ptr;
 static inline iree_status_t vm_buffer_load_f32(iree_vm_buffer_t* buffer,
                                                iree_host_size_t offset,
                                                float* result) {
-  return iree_vm_buffer_read_elements(buffer, offset * sizeof(*result), result,
-                                      1, sizeof(*result));
+  vm_buffer_load_f32_inline(buffer, offset, result);
+  return iree_ok_status();
 }
 
+#define vm_buffer_store_f32_inline(buffer, element_offset, value)        \
+  float* IREE_RESTRICT buffer_ptr = NULL;                                \
+  iree_vm_buffer_check_rw(buffer, element_offset, 1, float, buffer_ptr); \
+  *buffer_ptr = value;
 static inline iree_status_t vm_buffer_store_f32(iree_vm_buffer_t* buffer,
                                                 iree_host_size_t offset,
                                                 float value) {
-  return iree_vm_buffer_write_elements(&value, buffer, offset * sizeof(value),
-                                       1, sizeof(value));
+  vm_buffer_store_f32_inline(buffer, offset, value);
+  return iree_ok_status();
 }
 
 //===------------------------------------------------------------------===//
diff --git a/runtime/src/iree/vm/ref.h b/runtime/src/iree/vm/ref.h
index aa6e7fe..c2d1678 100644
--- a/runtime/src/iree/vm/ref.h
+++ b/runtime/src/iree/vm/ref.h
@@ -239,23 +239,27 @@
 
 // TODO(benvanik): make these macros standard/document them.
 #define IREE_VM_DECLARE_TYPE_ADAPTERS(name, T)                              \
+  IREE_API_EXPORT_VARIABLE iree_vm_ref_type_descriptor_t name##_descriptor; \
+  static inline iree_vm_ref_type_t name##_type_id() {                       \
+    return name##_descriptor.type;                                          \
+  }                                                                         \
+  static inline bool name##_isa(const iree_vm_ref_t ref) {                  \
+    return name##_descriptor.type == ref.type;                              \
+  }                                                                         \
   IREE_API_EXPORT iree_vm_ref_t name##_retain_ref(T* value);                \
   IREE_API_EXPORT iree_vm_ref_t name##_move_ref(T* value);                  \
-  IREE_API_EXPORT T* name##_deref(const iree_vm_ref_t ref);                 \
+  static inline T* name##_deref(const iree_vm_ref_t ref) {                  \
+    return IREE_LIKELY(name##_isa(ref)) ? (T*)ref.ptr : NULL;               \
+  }                                                                         \
   IREE_API_EXPORT iree_status_t name##_check_deref(const iree_vm_ref_t ref, \
                                                    T** out_ptr);            \
   IREE_API_EXPORT iree_status_t name##_check_deref_or_null(                 \
       const iree_vm_ref_t ref, T** out_ptr);                                \
-  IREE_API_EXPORT const iree_vm_ref_type_descriptor_t*                      \
-      name##_get_descriptor();                                              \
-  static inline bool name##_isa(const iree_vm_ref_t ref) {                  \
-    return name##_get_descriptor()->type == ref.type;                       \
-  }                                                                         \
-  IREE_API_EXPORT iree_vm_ref_type_t name##_type_id();                      \
   IREE_VM_DECLARE_CC_TYPE_LOOKUP(name, T)
 
 // TODO(benvanik): make these macros standard/document them.
 #define IREE_VM_DEFINE_TYPE_ADAPTERS(name, T)                               \
+  iree_vm_ref_type_descriptor_t name##_descriptor = {0};                    \
   IREE_API_EXPORT iree_vm_ref_t name##_retain_ref(T* value) {               \
     iree_vm_ref_t ref = {0};                                                \
     iree_vm_ref_wrap_retain(value, name##_descriptor.type, &ref);           \
@@ -266,13 +270,6 @@
     iree_vm_ref_wrap_assign(value, name##_descriptor.type, &ref);           \
     return ref;                                                             \
   }                                                                         \
-  IREE_API_EXPORT T* name##_deref(const iree_vm_ref_t ref) {                \
-    if (IREE_UNLIKELY(ref.type != ref.type) ||                              \
-        IREE_UNLIKELY(ref.type == IREE_VM_REF_TYPE_NULL)) {                 \
-      return NULL;                                                          \
-    }                                                                       \
-    return (T*)ref.ptr;                                                     \
-  }                                                                         \
   IREE_API_EXPORT iree_status_t name##_check_deref(const iree_vm_ref_t ref, \
                                                    T** out_ptr) {           \
     IREE_RETURN_IF_ERROR(iree_vm_ref_check(ref, name##_descriptor.type));   \
@@ -288,13 +285,6 @@
       *out_ptr = NULL;                                                      \
     }                                                                       \
     return iree_ok_status();                                                \
-  }                                                                         \
-  IREE_API_EXPORT const iree_vm_ref_type_descriptor_t*                      \
-      name##_get_descriptor() {                                             \
-    return &name##_descriptor;                                              \
-  }                                                                         \
-  IREE_API_EXPORT iree_vm_ref_type_t name##_type_id() {                     \
-    return name##_descriptor.type;                                          \
   }
 
 // Optional C++ iree::vm::ref<T> wrapper.
diff --git a/runtime/src/iree/vm/ref_cc.h b/runtime/src/iree/vm/ref_cc.h
index c43494a..f026fa9 100644
--- a/runtime/src/iree/vm/ref_cc.h
+++ b/runtime/src/iree/vm/ref_cc.h
@@ -473,16 +473,16 @@
 // dynamic type registration mechanism and that can be wrapped in an
 // iree_vm_ref_t.
 
-#define IREE_VM_DECLARE_CC_TYPE_LOOKUP(name, T)         \
-  namespace iree {                                      \
-  namespace vm {                                        \
-  template <>                                           \
-  struct ref_type_descriptor<T> {                       \
-    static const iree_vm_ref_type_descriptor_t* get() { \
-      return name##_get_descriptor();                   \
-    }                                                   \
-  };                                                    \
-  }                                                     \
+#define IREE_VM_DECLARE_CC_TYPE_LOOKUP(name, T)                \
+  namespace iree {                                             \
+  namespace vm {                                               \
+  template <>                                                  \
+  struct ref_type_descriptor<T> {                              \
+    static inline const iree_vm_ref_type_descriptor_t* get() { \
+      return &name##_descriptor;                               \
+    }                                                          \
+  };                                                           \
+  }                                                            \
   }
 
 #define IREE_VM_REGISTER_CC_TYPE(type, name, descriptor)  \
diff --git a/runtime/src/iree/vm/stack.c b/runtime/src/iree/vm/stack.c
index 6b39ed2..3d1b3a2 100644
--- a/runtime/src/iree/vm/stack.c
+++ b/runtime/src/iree/vm/stack.c
@@ -16,12 +16,6 @@
 #include "iree/base/tracing.h"
 #include "iree/vm/module.h"
 
-#ifndef NDEBUG
-#define VMCHECK(expr) assert(expr)
-#else
-#define VMCHECK(expr)
-#endif  // NDEBUG
-
 //===----------------------------------------------------------------------===//
 // Stack implementation
 //===----------------------------------------------------------------------===//
@@ -548,7 +542,7 @@
     iree_vm_stack_t* stack, const iree_vm_function_t* function,
     iree_vm_stack_frame_type_t frame_type, iree_host_size_t frame_size,
     iree_vm_stack_frame_cleanup_fn_t frame_cleanup_fn,
-    iree_vm_stack_frame_t** out_callee_frame) {
+    iree_vm_stack_frame_t* IREE_RESTRICT* out_callee_frame) {
   if (out_callee_frame) *out_callee_frame = NULL;
 
   // Allocate stack space and grow stack, if required.
diff --git a/runtime/src/iree/vm/stack.h b/runtime/src/iree/vm/stack.h
index f3aa557..7cb3253 100644
--- a/runtime/src/iree/vm/stack.h
+++ b/runtime/src/iree/vm/stack.h
@@ -307,7 +307,7 @@
     iree_vm_stack_t* stack, const iree_vm_function_t* function,
     iree_vm_stack_frame_type_t frame_type, iree_host_size_t frame_size,
     iree_vm_stack_frame_cleanup_fn_t frame_cleanup_fn,
-    iree_vm_stack_frame_t** out_callee_frame);
+    iree_vm_stack_frame_t* IREE_RESTRICT* out_callee_frame);
 
 // Leaves the current stack frame.
 IREE_API_EXPORT iree_status_t
diff --git a/runtime/src/iree/vm/test/buffer_ops.mlir b/runtime/src/iree/vm/test/buffer_ops.mlir
index 277595a..ddbf648 100644
--- a/runtime/src/iree/vm/test/buffer_ops.mlir
+++ b/runtime/src/iree/vm/test/buffer_ops.mlir
@@ -280,10 +280,8 @@
     // Fill the middle two elements.
     %c2 = vm.const.i64 1
     %c4 = vm.const.i64 2
-    %c2_byte = vm.mul.i64 %c2, %element_size : i64
-    %c4_byte = vm.mul.i64 %c4, %element_size : i64
     %value = vm.const.f32 42.0
-    vm.buffer.fill.f32 %buf_dno, %c2_byte, %c4_byte, %value : f32 -> !vm.buffer
+    vm.buffer.fill.f32 %buf_dno, %c2, %c4, %value : f32 -> !vm.buffer
 
     // Compare to reference.
     %c0 = vm.const.i64 0
@@ -295,7 +293,7 @@
   }
 
   vm.rodata private @test_fill_i8_ref  dense<[0, 102, 102, 0]> : tensor<4xi8>
-  
+
   // Tests filling a buffer with 8-bit values.
   vm.export @test_fill_i8
   vm.func @test_fill_i8() {
@@ -310,10 +308,8 @@
     // Fill the middle two elements.
     %c2 = vm.const.i64 1
     %c4 = vm.const.i64 2
-    %c2_byte = vm.mul.i64 %c2, %element_size : i64
-    %c4_byte = vm.mul.i64 %c4, %element_size : i64
     %value = vm.const.i32 102
-    vm.buffer.fill.i8 %buf_dno, %c2_byte, %c4_byte, %value : i32 -> !vm.buffer
+    vm.buffer.fill.i8 %buf_dno, %c2, %c4, %value : i32 -> !vm.buffer
 
     // Compare to reference.
     %c0 = vm.const.i64 0
@@ -325,7 +321,7 @@
   }
 
   vm.rodata private @test_fill_i16_ref dense<[0, 51966, 51966, 0]> : tensor<4xi16>
-  
+
   // Tests filling a buffer with 16-bit values.
   vm.export @test_fill_i16
   vm.func @test_fill_i16() {
@@ -340,10 +336,8 @@
     // Fill the middle two elements.
     %c2 = vm.const.i64 1
     %c4 = vm.const.i64 2
-    %c2_byte = vm.mul.i64 %c2, %element_size : i64
-    %c4_byte = vm.mul.i64 %c4, %element_size : i64
     %value = vm.const.i32 0xCAFE
-    vm.buffer.fill.i16 %buf_dno, %c2_byte, %c4_byte, %value : i32 -> !vm.buffer
+    vm.buffer.fill.i16 %buf_dno, %c2, %c4, %value : i32 -> !vm.buffer
 
     // Compare to reference.
     %c0 = vm.const.i64 0
@@ -355,7 +349,7 @@
   }
 
   vm.rodata private @test_fill_i32_ref dense<[0, 0xFFFF0000, 0xFFFF0000, 0]> : tensor<4xi32>
-  
+
   // Tests filling a buffer with 32-bit values.
   vm.export @test_fill_i32
   vm.func @test_fill_i32() {
@@ -370,10 +364,8 @@
     // Fill the middle two elements.
     %c2 = vm.const.i64 1
     %c4 = vm.const.i64 2
-    %c2_byte = vm.mul.i64 %c2, %element_size : i64
-    %c4_byte = vm.mul.i64 %c4, %element_size : i64
     %value = vm.const.i32 0xFFFF0000
-    vm.buffer.fill.i32 %buf_dno, %c2_byte, %c4_byte, %value : i32 -> !vm.buffer
+    vm.buffer.fill.i32 %buf_dno, %c2, %c4, %value : i32 -> !vm.buffer
 
     // Compare to reference.
     %c0 = vm.const.i64 0
@@ -385,7 +377,7 @@
   }
 
   vm.rodata private @test_fill_i64_ref dense<[0, 0x100000000, 0x100000000, 0]> : tensor<4xi64>
-  
+
   // Tests filling a buffer with 64-bit values.
   vm.export @test_fill_i64
   vm.func @test_fill_i64() {
@@ -400,10 +392,8 @@
     // Fill the middle two elements.
     %c2 = vm.const.i64 1
     %c4 = vm.const.i64 2
-    %c2_byte = vm.mul.i64 %c2, %element_size : i64
-    %c4_byte = vm.mul.i64 %c4, %element_size : i64
     %value = vm.const.i64 0x100000000
-    vm.buffer.fill.i64 %buf_dno, %c2_byte, %c4_byte, %value : i64 -> !vm.buffer
+    vm.buffer.fill.i64 %buf_dno, %c2, %c4, %value : i64 -> !vm.buffer
 
     // Compare to reference.
     %c0 = vm.const.i64 0
@@ -414,55 +404,6 @@
     vm.return
   }
 
-  vm.rodata private @test_fill_i16_misaligned_offset_ref dense<[0xCAFE, 0xCAFE, 0, 0]> : tensor<4xi16>
-
-  // Tests that misaligned fill offsets will succeed but round down.
-  vm.export @test_fill_i16_misaligned_offset
-  vm.func @test_fill_i16_misaligned_offset() {
-    // Allocate zeroed buffer.
-    %c8 = vm.const.i64 8
-    %buf = vm.buffer.alloc %c8 : !vm.buffer
-    %buf_dno = util.optimization_barrier %buf : !vm.buffer
-
-    // Try filling from offset 1, which is not i16-aligned.
-    %c1 = vm.const.i64 1
-    %c4 = vm.const.i64 4
-    %cafe = vm.const.i32 0xCAFE
-    vm.buffer.fill.i16 %buf_dno, %c1, %c4, %cafe : i32 -> !vm.buffer
-
-    // Compare to reference - should have written at offset 0.
-    %c0 = vm.const.i64 0
-    %rodata_ref = vm.const.ref.rodata @test_fill_i16_misaligned_offset_ref : !vm.buffer
-    %cmp = vm.buffer.compare %rodata_ref, %c0, %buf_dno, %c0, %c8 : !vm.buffer, !vm.buffer
-    vm.check.nz %cmp, "buffer should match reference" : i32
-
-    vm.return
-  }
-
-  vm.rodata private @test_fill_i16_misaligned_length_ref dense<[0, 0, 0, 0]> : tensor<4xi16>
-
-  // Tests that misaligned fill lengths will succeed but round down.
-  vm.export @test_fill_i16_misaligned_length
-  vm.func @test_fill_i16_misaligned_length() {
-    // Allocate zeroed buffer.
-    %c8 = vm.const.i64 8
-    %buf = vm.buffer.alloc %c8 : !vm.buffer
-    %buf_dno = util.optimization_barrier %buf : !vm.buffer
-
-    // Try filling for length 1, which is not i16-aligned.
-    %c0 = vm.const.i64 0
-    %c1 = vm.const.i64 1
-    %cafe = vm.const.i32 0xCAFE
-    vm.buffer.fill.i16 %buf_dno, %c0, %c1, %cafe : i32 -> !vm.buffer
-
-    // Compare to reference - should have written 0 bytes.
-    %rodata_ref = vm.const.ref.rodata @test_fill_i16_misaligned_length_ref : !vm.buffer
-    %cmp = vm.buffer.compare %rodata_ref, %c0, %buf_dno, %c0, %c8 : !vm.buffer, !vm.buffer
-    vm.check.nz %cmp, "buffer should match reference" : i32
-
-    vm.return
-  }
-
   // Tests that trying to fill .rodata will fail.
   vm.export @fail_fill_i16_rodata
   vm.func @fail_fill_i16_rodata() {
diff --git a/samples/custom_module/basic/module.cc b/samples/custom_module/basic/module.cc
index 6ba12d8..9f846d0 100644
--- a/samples/custom_module/basic/module.cc
+++ b/samples/custom_module/basic/module.cc
@@ -22,11 +22,6 @@
 // !custom.string type
 //===----------------------------------------------------------------------===//
 
-// Runtime type descriptor for the !custom.string describing how to manage it
-// and destroy it. The type ID is allocated at runtime and does not need to
-// match the compiler ID.
-static iree_vm_ref_type_descriptor_t iree_custom_string_descriptor = {0};
-
 // The "string" type we use to store and retain string data.
 // This could be arbitrarily complex or simply wrap another user-defined type.
 // The descriptor that is registered at startup defines how to manage the
@@ -43,6 +38,9 @@
   iree_string_view_t value;
 } iree_custom_string_t;
 
+// Runtime type descriptor for the !custom.string describing how to manage it
+// and destroy it. The type ID is allocated at runtime and does not need to
+// match the compiler ID.
 IREE_VM_DEFINE_TYPE_ADAPTERS(iree_custom_string, iree_custom_string_t);
 
 extern "C" iree_status_t iree_custom_string_create(
diff --git a/samples/simple_embedding/simple_embedding.c b/samples/simple_embedding/simple_embedding.c
index 3fa0c87..94272de 100644
--- a/samples/simple_embedding/simple_embedding.c
+++ b/samples/simple_embedding/simple_embedding.c
@@ -127,7 +127,7 @@
   // Get the result buffers from the invocation.
   iree_hal_buffer_view_t* ret_buffer_view =
       (iree_hal_buffer_view_t*)iree_vm_list_get_ref_deref(
-          outputs, 0, iree_hal_buffer_view_get_descriptor());
+          outputs, 0, &iree_hal_buffer_view_descriptor);
   if (ret_buffer_view == NULL) {
     return iree_make_status(IREE_STATUS_NOT_FOUND,
                             "can't find return buffer view");
diff --git a/tests/compiler_driver/smoketest.mlir b/tests/compiler_driver/smoketest.mlir
index 9f21cb3..b7fc954 100644
--- a/tests/compiler_driver/smoketest.mlir
+++ b/tests/compiler_driver/smoketest.mlir
@@ -10,7 +10,10 @@
 // CHECK: "function_descriptors":
 // CHECK-NEXT: {
 // CHECK-NEXT:   "bytecode_offset": 0
-// CHECK-NEXT:   "bytecode_length": 8
+// CHECK-NEXT:   "bytecode_length": 6
+// CHECK-NEXT:   "requirements": 0
+// CHECK-NEXT:   "reserved": 0
+// CHECK-NEXT:   "block_count": 1
 // CHECK-NEXT:   "i32_register_count": 1
 // CHECK-NEXT:   "ref_register_count": 0
 // CHECK-NEXT: }
@@ -19,11 +22,12 @@
 }
 
 // CHECK: "bytecode_data": [
+// CHECK-NEXT:   121,
 // CHECK-NEXT:   90,
-// CHECK-NEXT:   0,
 // CHECK-NEXT:   1,
 // CHECK-NEXT:   0,
 // CHECK-NEXT:   0,
+// CHECK-NEXT:   0,
 }
 
 // -----
diff --git a/tools/iree-dump-module-main.c b/tools/iree-dump-module-main.c
index 9f4a9a0..c769270 100644
--- a/tools/iree-dump-module-main.c
+++ b/tools/iree-dump-module-main.c
@@ -9,6 +9,7 @@
 #include "iree/base/api.h"
 #include "iree/base/internal/file_io.h"
 #include "iree/schemas/bytecode_module_def_json_printer.h"
+#include "iree/vm/bytecode/archive.h"
 #include "iree/vm/bytecode/module.h"
 
 // Today we just print to JSON. We could do something more useful (size
@@ -28,7 +29,7 @@
                                         &file_contents));
 
   iree_const_byte_span_t flatbuffer_contents = iree_const_byte_span_empty();
-  IREE_CHECK_OK(iree_vm_bytecode_module_parse_header(
+  IREE_CHECK_OK(iree_vm_bytecode_archive_parse_header(
       file_contents->const_buffer, &flatbuffer_contents,
       /*out_rodata_offset=*/NULL));
 
diff --git a/tools/iree-e2e-matmul-test.c b/tools/iree-e2e-matmul-test.c
index 217d4e6..e77a89c 100644
--- a/tools/iree-e2e-matmul-test.c
+++ b/tools/iree-e2e-matmul-test.c
@@ -27,6 +27,132 @@
 
 static const char* emoji(bool good) { return good ? "🦄" : "🐞"; }
 
+// Defines the type of a primitive value.
+typedef enum iree_e2e_test_value_type_e {
+  // Not a value type.
+  IREE_E2E_TEST_VALUE_TYPE_NONE = 0,
+  // int8_t.
+  IREE_E2E_TEST_VALUE_TYPE_I8 = 1,
+  // int16_t.
+  IREE_E2E_TEST_VALUE_TYPE_I16 = 2,
+  // int32_t.
+  IREE_E2E_TEST_VALUE_TYPE_I32 = 3,
+  // int64_t.
+  IREE_E2E_TEST_VALUE_TYPE_I64 = 4,
+  // halft_t.
+  IREE_E2E_TEST_VALUE_TYPE_F16 = 5,
+  // float.
+  IREE_E2E_TEST_VALUE_TYPE_F32 = 6,
+  // double.
+  IREE_E2E_TEST_VALUE_TYPE_F64 = 7,
+} iree_e2e_test_value_type_t;
+
+// Maximum size, in bytes, of any value type we can represent.
+#define IREE_E2E_TEST_VALUE_STORAGE_SIZE 8
+
+// A variant value type.
+typedef struct iree_e2e_test_value_t {
+  iree_e2e_test_value_type_t type;
+  union {
+    int8_t i8;
+    int16_t i16;
+    int32_t i32;
+    int64_t i64;
+    float f32;
+    uint16_t f16_u16;
+    double f64;
+    uint8_t value_storage[IREE_E2E_TEST_VALUE_STORAGE_SIZE];  // max size of all
+                                                              // value types
+  };
+} iree_e2e_test_value_t;
+
+static inline iree_e2e_test_value_t iree_e2e_test_value_make_none() {
+  iree_e2e_test_value_t result;
+  result.type = IREE_E2E_TEST_VALUE_TYPE_NONE;
+  return result;
+}
+
+static inline iree_e2e_test_value_t iree_e2e_test_value_make_i8(int8_t value) {
+  iree_e2e_test_value_t result;
+  result.type = IREE_E2E_TEST_VALUE_TYPE_I8;
+  result.i8 = value;
+  return result;
+}
+
+static inline iree_e2e_test_value_t iree_e2e_test_value_make_i16(
+    int16_t value) {
+  iree_e2e_test_value_t result;
+  result.type = IREE_E2E_TEST_VALUE_TYPE_I16;
+  result.i16 = value;
+  return result;
+}
+
+static inline iree_e2e_test_value_t iree_e2e_test_value_make_i32(
+    int32_t value) {
+  iree_e2e_test_value_t result;
+  result.type = IREE_E2E_TEST_VALUE_TYPE_I32;
+  result.i32 = value;
+  return result;
+}
+
+// TODO(#5542): check the value type before accessing the union.
+static inline int32_t iree_e2e_test_value_get_i32(
+    iree_e2e_test_value_t* value) {
+  return value->i32;
+}
+
+static inline iree_e2e_test_value_t iree_e2e_test_value_make_i64(
+    int64_t value) {
+  iree_e2e_test_value_t result;
+  result.type = IREE_E2E_TEST_VALUE_TYPE_I64;
+  result.i64 = value;
+  return result;
+}
+
+// TODO(#5542): check the value type before accessing the union.
+static inline int64_t iree_e2e_test_value_get_i64(
+    iree_e2e_test_value_t* value) {
+  return value->i64;
+}
+
+static inline iree_e2e_test_value_t iree_e2e_test_value_make_f16(
+    uint16_t value) {
+  iree_e2e_test_value_t result;
+  result.type = IREE_E2E_TEST_VALUE_TYPE_F16;
+  result.f16_u16 = value;
+  return result;
+}
+
+static inline iree_e2e_test_value_t iree_e2e_test_value_make_f32(float value) {
+  iree_e2e_test_value_t result;
+  result.type = IREE_E2E_TEST_VALUE_TYPE_F32;
+  result.f32 = value;
+  return result;
+}
+
+// TODO(#5542): check the value type before accessing the union.
+static inline float iree_e2e_test_value_get_f32(iree_e2e_test_value_t* value) {
+  return value->f32;
+}
+
+// TODO(#5542): check the value type before accessing the union.
+static inline uint16_t iree_e2e_test_value_get_f16(
+    iree_e2e_test_value_t* value) {
+  return value->f16_u16;
+}
+
+static inline iree_e2e_test_value_t iree_e2e_test_value_make_f64(double value) {
+  iree_e2e_test_value_t result;
+  result.type = IREE_E2E_TEST_VALUE_TYPE_F64;
+  result.f64 = value;
+  return result;
+}
+
+// TODO(#5542): check the value type before accessing the union.
+static inline double iree_e2e_test_value_get_f64(iree_e2e_test_value_t* value) {
+  return value->f64;
+}
+
 /*****************************************************************************
  *
  * Part 1:
diff --git a/tools/iree-run-mlir-main.cc b/tools/iree-run-mlir-main.cc
index 1a252e8..d7830fb 100644
--- a/tools/iree-run-mlir-main.cc
+++ b/tools/iree-run-mlir-main.cc
@@ -285,12 +285,14 @@
   // NOTE: if we have an output file specified then we could compile into that
   // for greater efficiency. Today we assume that users aren't passing multi-GB
   // models through this tool (or if they are they have the memory to run them).
+  auto vm_options =
+      mlir::iree_compiler::IREE::VM::TargetOptions::FromFlags::get();
   auto bytecode_options =
       mlir::iree_compiler::IREE::VM::BytecodeTargetOptions::FromFlags::get();
   std::string binary_contents;
   llvm::raw_string_ostream binary_output(binary_contents);
   if (failed(mlir::iree_compiler::IREE::VM::translateModuleToBytecode(
-          mlir_module.get(), bytecode_options, binary_output))) {
+          mlir_module.get(), vm_options, bytecode_options, binary_output))) {
     return iree_make_status(
         IREE_STATUS_INTERNAL,
         "serialization to flatbuffer bytecode (binary) failed");
@@ -305,7 +307,7 @@
     std::string text_contents;
     llvm::raw_string_ostream text_output(text_contents);
     if (failed(mlir::iree_compiler::IREE::VM::translateModuleToBytecode(
-            mlir_module.get(), bytecode_options, text_output))) {
+            mlir_module.get(), vm_options, bytecode_options, text_output))) {
       return iree_make_status(IREE_STATUS_INTERNAL,
                               "serialization to annotated MLIR (text) failed");
     }
@@ -318,7 +320,7 @@
     std::string text_contents;
     llvm::raw_string_ostream text_output(text_contents);
     if (failed(mlir::iree_compiler::IREE::VM::translateModuleToBytecode(
-            mlir_module.get(), bytecode_options, text_output))) {
+            mlir_module.get(), vm_options, bytecode_options, text_output))) {
       return iree_make_status(
           IREE_STATUS_INTERNAL,
           "serialization to flatbuffer bytecode (text) failed");