Adding iree_vm_list_swap_storage and iree_vm_list_copy. (#12088)

This covers the C implementation of these routines and adds a bunch of
tests. Future changes will expose these to the compiler as VM ops.

Progress on #7014.
diff --git a/runtime/src/iree/vm/list.c b/runtime/src/iree/vm/list.c
index 689b228..d7bdef5 100644
--- a/runtime/src/iree/vm/list.c
+++ b/runtime/src/iree/vm/list.c
@@ -291,6 +291,7 @@
 
 IREE_API_EXPORT iree_status_t
 iree_vm_list_reserve(iree_vm_list_t* list, iree_host_size_t minimum_capacity) {
+  IREE_ASSERT_ARGUMENT(list);
   if (list->capacity >= minimum_capacity) {
     return iree_ok_status();
   }
@@ -305,11 +306,13 @@
 }
 
 IREE_API_EXPORT iree_host_size_t iree_vm_list_size(const iree_vm_list_t* list) {
+  IREE_ASSERT_ARGUMENT(list);
   return list->count;
 }
 
 IREE_API_EXPORT iree_status_t iree_vm_list_resize(iree_vm_list_t* list,
                                                   iree_host_size_t new_size) {
+  IREE_ASSERT_ARGUMENT(list);
   if (new_size == list->count) {
     return iree_ok_status();
   } else if (new_size < list->count) {
@@ -333,6 +336,228 @@
   list->count = 0;
 }
 
+static void iree_memswap(void* a, void* b, iree_host_size_t size) {
+  uint8_t* a_ptr = (uint8_t*)a;
+  uint8_t* b_ptr = (uint8_t*)b;
+  for (iree_host_size_t i = 0; i < size; ++i) {
+    uint8_t t = a_ptr[i];
+    a_ptr[i] = b_ptr[i];
+    b_ptr[i] = t;
+  }
+}
+
+IREE_API_EXPORT void iree_vm_list_swap_storage(iree_vm_list_t* list_a,
+                                               iree_vm_list_t* list_b) {
+  IREE_ASSERT_ARGUMENT(list_a);
+  IREE_ASSERT_ARGUMENT(list_b);
+  if (list_a == list_b) return;
+  iree_memswap(&list_a->allocator, &list_b->allocator,
+               sizeof(list_a->allocator));
+  iree_memswap(&list_a->capacity, &list_b->capacity, sizeof(list_a->capacity));
+  iree_memswap(&list_a->count, &list_b->count, sizeof(list_a->count));
+  iree_memswap(&list_a->element_type, &list_b->element_type,
+               sizeof(list_a->element_type));
+  iree_memswap(&list_a->element_size, &list_b->element_size,
+               sizeof(list_a->element_size));
+  iree_memswap(&list_a->storage_mode, &list_b->storage_mode,
+               sizeof(list_a->storage_mode));
+  iree_memswap(&list_a->storage, &list_b->storage, sizeof(list_a->storage));
+}
+
+// Returns true if |src_type| can be converted into |dst_type|.
+static bool iree_vm_type_def_is_compatible(iree_vm_type_def_t src_type,
+                                           iree_vm_type_def_t dst_type) {
+  return memcmp(&src_type, &dst_type, sizeof(dst_type)) == 0;
+}
+
+// Copies from a |src_list| of any type (value, ref, variant) into a |dst_list|
+// in variant storage mode. This cannot fail as variant lists can store any
+// type.
+static void iree_vm_list_copy_to_variant_list(iree_vm_list_t* src_list,
+                                              iree_host_size_t src_i,
+                                              iree_vm_list_t* dst_list,
+                                              iree_host_size_t dst_i,
+                                              iree_host_size_t count) {
+  iree_vm_variant_t* dst_storage =
+      (iree_vm_variant_t*)dst_list->storage + dst_i;
+  switch (src_list->storage_mode) {
+    case IREE_VM_LIST_STORAGE_MODE_VALUE: {
+      uintptr_t src_storage =
+          (uintptr_t)src_list->storage + src_i * src_list->element_size;
+      for (iree_host_size_t i = 0; i < count; ++i) {
+        if (iree_vm_type_def_is_ref(&dst_storage[i].type)) {
+          iree_vm_ref_release(&dst_storage[i].ref);
+        }
+        dst_storage[i].type = src_list->element_type;
+        memcpy(dst_storage[i].value_storage,
+               (uint8_t*)src_storage + i * src_list->element_size,
+               src_list->element_size);
+      }
+      break;
+    }
+    case IREE_VM_LIST_STORAGE_MODE_REF: {
+      iree_vm_ref_t* src_storage = (iree_vm_ref_t*)src_list->storage + src_i;
+      for (iree_host_size_t i = 0; i < count; ++i) {
+        // NOTE: we retain first in case the lists alias and the ref is the
+        // same.
+        iree_vm_ref_t* ref = &src_storage[i];
+        iree_vm_ref_retain_inplace(ref);
+        if (iree_vm_type_def_is_ref(&dst_storage[i].type)) {
+          iree_vm_ref_release(&dst_storage[i].ref);
+        }
+        dst_storage->type = iree_vm_type_def_make_ref_type(ref->type);
+        dst_storage->ref = *ref;
+      }
+      break;
+    }
+    case IREE_VM_LIST_STORAGE_MODE_VARIANT: {
+      iree_vm_variant_t* src_storage =
+          (iree_vm_variant_t*)src_list->storage + src_i;
+      for (iree_host_size_t i = 0; i < count; ++i) {
+        // NOTE: we retain first in case the lists alias and the ref is the
+        // same.
+        if (iree_vm_type_def_is_ref(&src_storage[i].type)) {
+          iree_vm_ref_retain_inplace(&src_storage[i].ref);
+        }
+        if (iree_vm_type_def_is_ref(&dst_storage[i].type)) {
+          iree_vm_ref_release(&dst_storage[i].ref);
+        }
+        memcpy(&dst_storage[i], &src_storage[i], sizeof(dst_storage[i]));
+      }
+      break;
+    }
+  }
+}
+
+// Copies from a |src_list| in variant storage mode to a |dst_list| of any type
+// (value, ref) while checking each element. This first needs to ensure the
+// entire source range matches the expected destination type which makes this
+// much slower than the other paths that need not check or only check once per
+// copy operation instead.
+static iree_status_t iree_vm_list_copy_from_variant_list(
+    iree_vm_list_t* src_list, iree_host_size_t src_i, iree_vm_list_t* dst_list,
+    iree_host_size_t dst_i, iree_host_size_t count) {
+  iree_vm_variant_t* src_storage =
+      (iree_vm_variant_t*)src_list->storage + src_i;
+  switch (dst_list->storage_mode) {
+    case IREE_VM_LIST_STORAGE_MODE_VALUE:
+    case IREE_VM_LIST_STORAGE_MODE_REF:
+      for (iree_host_size_t i = 0; i < count; ++i) {
+        if (!iree_vm_type_def_is_compatible(src_storage[i].type,
+                                            dst_list->element_type)) {
+          return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                                  "destination list element type does not "
+                                  "match the source element %" PRIhsz,
+                                  src_i + i);
+        }
+      }
+      break;
+    default:
+      // Destination is a variant list and accepts all inputs.
+      break;
+  }
+  switch (dst_list->storage_mode) {
+    case IREE_VM_LIST_STORAGE_MODE_VALUE: {
+      uintptr_t dst_storage =
+          (uintptr_t)dst_list->storage + dst_i * dst_list->element_size;
+      for (iree_host_size_t i = 0; i < count; ++i) {
+        memcpy((uint8_t*)dst_storage + i * dst_list->element_size,
+               src_storage[i].value_storage, dst_list->element_size);
+      }
+      break;
+    }
+    case IREE_VM_LIST_STORAGE_MODE_REF: {
+      iree_vm_ref_t* dst_storage = (iree_vm_ref_t*)dst_list->storage + dst_i;
+      for (iree_host_size_t i = 0; i < count; ++i) {
+        iree_vm_ref_retain(&src_storage[i].ref, &dst_storage[i]);
+      }
+      break;
+    }
+    default:
+    case IREE_VM_LIST_STORAGE_MODE_VARIANT:
+      return iree_make_status(IREE_STATUS_FAILED_PRECONDITION,
+                              "unhandled copy mode");
+  }
+  return iree_ok_status();
+}
+
+IREE_API_EXPORT iree_status_t iree_vm_list_copy(iree_vm_list_t* src_list,
+                                                iree_host_size_t src_i,
+                                                iree_vm_list_t* dst_list,
+                                                iree_host_size_t dst_i,
+                                                iree_host_size_t count) {
+  IREE_ASSERT_ARGUMENT(src_list);
+  IREE_ASSERT_ARGUMENT(dst_list);
+
+  // Fast-path no-op check.
+  if (count == 0) return iree_ok_status();
+
+  // Verify ranges.
+  const iree_host_size_t src_count = iree_vm_list_size(src_list);
+  if (src_i + count > src_count) {
+    return iree_make_status(
+        IREE_STATUS_OUT_OF_RANGE,
+        "source range [%" PRIhsz ", %" PRIhsz ") of %" PRIhsz
+        " elements out of range of source list with size %" PRIhsz,
+        src_i, src_i + count, count, src_count);
+  }
+  const iree_host_size_t dst_count = iree_vm_list_size(dst_list);
+  if (dst_i + count > dst_count) {
+    return iree_make_status(
+        IREE_STATUS_OUT_OF_RANGE,
+        "destination range [%" PRIhsz ", %" PRIhsz ") of %" PRIhsz
+        " elements out of range of destination list with size %" PRIhsz,
+        dst_i, dst_i + count, count, dst_count);
+  }
+
+  // Prevent overlap when copying within the same list.
+  if (src_list == dst_list && src_i + count > dst_i && dst_i + count > src_i) {
+    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                            "overlapping copy of range [%" PRIhsz ", %" PRIhsz
+                            ") to [%" PRIhsz ", %" PRIhsz ") of %" PRIhsz
+                            " elements not supported",
+                            src_i, src_i + count, dst_i, dst_i + count, count);
+  }
+
+  // Copies into variant lists is a slow-path as we need to check the type of
+  // each element we copy. Note that the source of the copy can be of any type.
+  // When copying in the other direction of a variant list to a typed list we
+  // need to ensure all copied elements match the expected destination type.
+  if (dst_list->storage_mode == IREE_VM_LIST_STORAGE_MODE_VARIANT) {
+    iree_vm_list_copy_to_variant_list(src_list, src_i, dst_list, dst_i, count);
+    return iree_ok_status();
+  } else if (src_list->storage_mode == IREE_VM_LIST_STORAGE_MODE_VARIANT) {
+    return iree_vm_list_copy_from_variant_list(src_list, src_i, dst_list, dst_i,
+                                               count);
+  }
+
+  // If neither source or destination are variant lists we need to match the
+  // types exactly.
+  if (src_list->storage_mode != dst_list->storage_mode ||
+      memcmp(&src_list->element_type, &dst_list->element_type,
+             sizeof(src_list->element_type)) != 0) {
+    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                            "src/dst element type mismatch");
+  }
+
+  if (src_list->storage_mode == IREE_VM_LIST_STORAGE_MODE_VALUE) {
+    // Memcpy primitive values fast path.
+    memcpy((uint8_t*)dst_list->storage + dst_i * dst_list->element_size,
+           (uint8_t*)src_list->storage + src_i * src_list->element_size,
+           count * dst_list->element_size);
+  } else {
+    // Retain ref fast(ish) path - note that iree_vm_ref_retain will release
+    // any existing value in the dest list it overwrites.
+    iree_vm_ref_t* src_ref_storage = (iree_vm_ref_t*)src_list->storage + src_i;
+    iree_vm_ref_t* dst_ref_storage = (iree_vm_ref_t*)dst_list->storage + dst_i;
+    for (iree_host_size_t i = 0; i < count; ++i) {
+      iree_vm_ref_retain(src_ref_storage + i, dst_ref_storage + i);
+    }
+  }
+
+  return iree_ok_status();
+}
+
 static void iree_vm_list_convert_value_type(
     const iree_vm_value_t* source_value, iree_vm_value_type_t target_value_type,
     iree_vm_value_t* out_value) {
@@ -720,7 +945,7 @@
       iree_vm_ref_t* element_ref = (iree_vm_ref_t*)element_ptr;
       out_value->type.ref_type = element_ref->type;
       out_value->type.value_type = IREE_VM_VALUE_TYPE_NONE;
-      iree_vm_ref_retain(element_ref, &out_value->ref);
+      iree_vm_ref_assign(element_ref, &out_value->ref);
       break;
     }
     case IREE_VM_LIST_STORAGE_MODE_VARIANT: {
diff --git a/runtime/src/iree/vm/list.h b/runtime/src/iree/vm/list.h
index 361b2a8..b7d2ba3 100644
--- a/runtime/src/iree/vm/list.h
+++ b/runtime/src/iree/vm/list.h
@@ -104,6 +104,32 @@
 // Clears the list contents. Equivalent to resizing to 0.
 IREE_API_EXPORT void iree_vm_list_clear(iree_vm_list_t* list);
 
+// Swaps the storage of |list_a| and |list_b|. The list references remain the
+// same but the count, capacity, and underlying storage will be swapped. This
+// can be used to treat lists as persistent stable references to dynamically
+// mutated storage such as when emulating structs or dicts.
+//
+// WARNING: if a list is initialized in-place with iree_vm_list_initialize this
+// will still perform the storage swap but may lead to unexpected issues if the
+// lifetime of the storage is shorter than the lifetime of the newly-swapped
+// list.
+IREE_API_EXPORT void iree_vm_list_swap_storage(iree_vm_list_t* list_a,
+                                               iree_vm_list_t* list_b);
+
+// Copies |count| elements from |src_list| starting at |src_i| to |dst_list|
+// starting at |dst_i|. The ranges specified must be valid in both lists.
+//
+// Supported list types:
+//   any type -> variant list
+//   variant list -> compatible element types only
+//   same value type -> same value type
+//   same ref type -> same ref type
+IREE_API_EXPORT iree_status_t iree_vm_list_copy(iree_vm_list_t* src_list,
+                                                iree_host_size_t src_i,
+                                                iree_vm_list_t* dst_list,
+                                                iree_host_size_t dst_i,
+                                                iree_host_size_t count);
+
 // Returns the value of the element at the given index.
 // Note that the value type may vary from element to element in variant lists
 // and callers should check the |out_value| type.
diff --git a/runtime/src/iree/vm/list_test.cc b/runtime/src/iree/vm/list_test.cc
index 2ee71f2..e9b1d08 100644
--- a/runtime/src/iree/vm/list_test.cc
+++ b/runtime/src/iree/vm/list_test.cc
@@ -39,10 +39,97 @@
 IREE_VM_DECLARE_TYPE_ADAPTERS(test_b, B);
 IREE_VM_DEFINE_TYPE_ADAPTERS(test_b, B);
 
+static bool operator==(const iree_vm_value_t& lhs,
+                       const iree_vm_value_t& rhs) noexcept {
+  if (lhs.type != rhs.type) return false;
+  switch (lhs.type) {
+    default:
+    case IREE_VM_VALUE_TYPE_NONE:
+      return true;  // none == none
+    case IREE_VM_VALUE_TYPE_I8:
+      return lhs.i8 == rhs.i8;
+    case IREE_VM_VALUE_TYPE_I16:
+      return lhs.i16 == rhs.i16;
+    case IREE_VM_VALUE_TYPE_I32:
+      return lhs.i32 == rhs.i32;
+    case IREE_VM_VALUE_TYPE_I64:
+      return lhs.i64 == rhs.i64;
+    case IREE_VM_VALUE_TYPE_F32:
+      return lhs.f32 == rhs.f32;
+    case IREE_VM_VALUE_TYPE_F64:
+      return lhs.f64 == rhs.f64;
+  }
+}
+
+static std::ostream& operator<<(std::ostream& os,
+                                const iree_vm_value_t& value) {
+  switch (value.type) {
+    default:
+    case IREE_VM_VALUE_TYPE_NONE:
+      return os << "??";
+    case IREE_VM_VALUE_TYPE_I8:
+      return os << value.i8;
+    case IREE_VM_VALUE_TYPE_I16:
+      return os << value.i16;
+    case IREE_VM_VALUE_TYPE_I32:
+      return os << value.i32;
+    case IREE_VM_VALUE_TYPE_I64:
+      return os << value.i64;
+    case IREE_VM_VALUE_TYPE_F32:
+      return os << value.f32;
+    case IREE_VM_VALUE_TYPE_F64:
+      return os << value.f64;
+  }
+}
+
+template <size_t N>
+static std::vector<iree_vm_value_t> MakeValuesList(const int32_t (&values)[N]) {
+  std::vector<iree_vm_value_t> result;
+  result.resize(N);
+  for (size_t i = 0; i < N; ++i) result[i] = iree_vm_value_make_i32(values[i]);
+  return result;
+}
+
+template <size_t N>
+static std::vector<iree_vm_value_t> MakeValuesList(const float (&values)[N]) {
+  std::vector<iree_vm_value_t> result;
+  result.resize(N);
+  for (size_t i = 0; i < N; ++i) result[i] = iree_vm_value_make_f32(values[i]);
+  return result;
+}
+
+static std::vector<iree_vm_value_t> GetValuesList(iree_vm_list_t* list) {
+  std::vector<iree_vm_value_t> result;
+  result.resize(iree_vm_list_size(list));
+  for (iree_host_size_t i = 0; i < result.size(); ++i) {
+    iree_vm_variant_t variant = iree_vm_variant_empty();
+    IREE_CHECK_OK(iree_vm_list_get_variant(list, i, &variant));
+    if (iree_vm_type_def_is_value(&variant.type)) {
+      result[i].type = variant.type.value_type;
+      memcpy(result[i].value_storage, variant.value_storage,
+             sizeof(result[i].value_storage));
+    } else if (iree_vm_type_def_is_ref(&variant.type)) {
+      if (test_a_isa(variant.ref)) {
+        result[i] = iree_vm_value_make_f32(test_a_deref(variant.ref)->data());
+      } else if (test_b_isa(variant.ref)) {
+        result[i] = iree_vm_value_make_i32(test_b_deref(variant.ref)->data());
+      }
+    }
+  }
+  return result;
+}
+
+static bool operator==(const iree_vm_ref_t& lhs,
+                       const iree_vm_ref_t& rhs) noexcept {
+  return lhs.type == rhs.type && lhs.ptr == rhs.ptr;
+}
+
 namespace {
 
 using ::iree::Status;
+using ::iree::StatusCode;
 using ::iree::testing::status::StatusIs;
+using testing::Eq;
 
 template <typename T>
 static void RegisterRefType(iree_vm_ref_type_descriptor_t* descriptor,
@@ -70,7 +157,7 @@
   return ref;
 }
 
-static iree_vm_instance_t* instance = NULL;
+static iree_vm_instance_t* instance = nullptr;
 struct VMListTest : public ::testing::Test {
   static void SetUpTestSuite() {
     IREE_CHECK_OK(iree_vm_instance_create(iree_allocator_system(), &instance));
@@ -208,7 +295,7 @@
                                      iree_allocator_system(), &source_list));
 
   // Clone list.
-  iree_vm_list_t* target_list = NULL;
+  iree_vm_list_t* target_list = nullptr;
   IREE_ASSERT_OK(
       iree_vm_list_clone(source_list, iree_allocator_system(), &target_list));
 
@@ -241,7 +328,7 @@
   }
 
   // Clone list.
-  iree_vm_list_t* target_list = NULL;
+  iree_vm_list_t* target_list = nullptr;
   IREE_ASSERT_OK(
       iree_vm_list_clone(source_list, iree_allocator_system(), &target_list));
 
@@ -268,7 +355,7 @@
                                      &source_list));
 
   // Clone list.
-  iree_vm_list_t* target_list = NULL;
+  iree_vm_list_t* target_list = nullptr;
   IREE_ASSERT_OK(
       iree_vm_list_clone(source_list, iree_allocator_system(), &target_list));
 
@@ -299,7 +386,7 @@
   }
 
   // Clone list.
-  iree_vm_list_t* target_list = NULL;
+  iree_vm_list_t* target_list = nullptr;
   IREE_ASSERT_OK(
       iree_vm_list_clone(source_list, iree_allocator_system(), &target_list));
 
@@ -330,7 +417,7 @@
                                      &source_list));
 
   // Clone list.
-  iree_vm_list_t* target_list = NULL;
+  iree_vm_list_t* target_list = nullptr;
   IREE_ASSERT_OK(
       iree_vm_list_clone(source_list, iree_allocator_system(), &target_list));
 
@@ -364,7 +451,7 @@
   }
 
   // Clone list.
-  iree_vm_list_t* target_list = NULL;
+  iree_vm_list_t* target_list = nullptr;
   IREE_ASSERT_OK(
       iree_vm_list_clone(source_list, iree_allocator_system(), &target_list));
 
@@ -577,6 +664,541 @@
   iree_vm_list_release(list);
 }
 
+// Tests that swapping the storage of a list with itself is a no-op.
+TEST_F(VMListTest, SwapStorageSelf) {
+  iree_vm_type_def_t element_type =
+      iree_vm_type_def_make_ref_type(test_a_type_id());
+  iree_vm_list_t* list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(&element_type, /*initial_capacity=*/8,
+                                     iree_allocator_system(), &list));
+  for (iree_host_size_t i = 0; i < 5; ++i) {
+    iree_vm_ref_t ref_a = MakeRef<A>((float)i);
+    IREE_ASSERT_OK(iree_vm_list_push_ref_move(list, &ref_a));
+  }
+
+  iree_vm_list_swap_storage(list, list);
+
+  for (iree_host_size_t i = 0; i < 5; ++i) {
+    iree_vm_ref_t ref_a{0};
+    IREE_ASSERT_OK(iree_vm_list_get_ref_retain(list, i, &ref_a));
+    EXPECT_TRUE(test_a_isa(ref_a));
+    auto* a = test_a_deref(ref_a);
+    EXPECT_EQ(i, a->data());
+    iree_vm_ref_release(&ref_a);
+  }
+
+  iree_vm_list_release(list);
+}
+
+// Tests swapping the storage of two lists with different types. The lists
+// should have their types, size, and storage swapped.
+TEST_F(VMListTest, SwapStorage) {
+  iree_vm_type_def_t element_type_a =
+      iree_vm_type_def_make_ref_type(test_a_type_id());
+  iree_vm_list_t* list_a = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(&element_type_a, /*initial_capacity=*/8,
+                                     iree_allocator_system(), &list_a));
+  iree_host_size_t list_a_size = 4;
+  for (iree_host_size_t i = 0; i < list_a_size; ++i) {
+    iree_vm_ref_t ref_a = MakeRef<A>((float)i);
+    IREE_ASSERT_OK(iree_vm_list_push_ref_move(list_a, &ref_a));
+  }
+
+  iree_vm_type_def_t element_type_b =
+      iree_vm_type_def_make_ref_type(test_b_type_id());
+  iree_vm_list_t* list_b = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(&element_type_b, /*initial_capacity=*/8,
+                                     iree_allocator_system(), &list_b));
+  iree_host_size_t list_b_size = 3;
+  for (iree_host_size_t i = 0; i < list_b_size; ++i) {
+    iree_vm_ref_t ref_b = MakeRef<B>((float)i);
+    IREE_ASSERT_OK(iree_vm_list_push_ref_move(list_b, &ref_b));
+  }
+
+  iree_vm_list_swap_storage(list_a, list_b);
+
+  // list_a should have b types.
+  iree_vm_type_def_t queried_type_a = iree_vm_list_element_type(list_a);
+  EXPECT_EQ(0,
+            memcmp(&queried_type_a, &element_type_b, sizeof(element_type_b)));
+  EXPECT_EQ(iree_vm_list_size(list_a), list_b_size);
+  for (iree_host_size_t i = 0; i < list_b_size; ++i) {
+    iree_vm_ref_t ref_b{0};
+    IREE_ASSERT_OK(iree_vm_list_get_ref_retain(list_a, i, &ref_b));
+    EXPECT_TRUE(test_b_isa(ref_b));
+    auto* b = test_b_deref(ref_b);
+    EXPECT_EQ(i, b->data());
+    iree_vm_ref_release(&ref_b);
+  }
+
+  // list_b should have a types.
+  iree_vm_type_def_t queried_type_b = iree_vm_list_element_type(list_b);
+  EXPECT_EQ(0,
+            memcmp(&queried_type_b, &element_type_a, sizeof(element_type_a)));
+  EXPECT_EQ(iree_vm_list_size(list_b), list_a_size);
+  for (iree_host_size_t i = 0; i < list_b_size; ++i) {
+    iree_vm_ref_t ref_a{0};
+    IREE_ASSERT_OK(iree_vm_list_get_ref_retain(list_b, i, &ref_a));
+    EXPECT_TRUE(test_a_isa(ref_a));
+    auto* a = test_a_deref(ref_a);
+    EXPECT_EQ(i, a->data());
+    iree_vm_ref_release(&ref_a);
+  }
+
+  iree_vm_list_release(list_a);
+  iree_vm_list_release(list_b);
+}
+
+// Tests the boundary conditions around out-of-range copies.
+// All of the logic for this is shared across all the various copy modes.
+TEST_F(VMListTest, CopyOutOfRange) {
+  iree_vm_type_def_t element_type =
+      iree_vm_type_def_make_value_type(IREE_VM_VALUE_TYPE_I32);
+  iree_vm_list_t* src_list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(&element_type, /*initial_capacity=*/8,
+                                     iree_allocator_system(), &src_list));
+  iree_vm_list_t* dst_list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(&element_type, /*initial_capacity=*/8,
+                                     iree_allocator_system(), &dst_list));
+
+  // Lists are both empty - everything should fail.
+  IREE_ASSERT_OK(iree_vm_list_resize(src_list, 0));
+  IREE_ASSERT_OK(iree_vm_list_resize(dst_list, 0));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 100, 1)),
+              StatusIs(StatusCode::kOutOfRange));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 0, 100)),
+              StatusIs(StatusCode::kOutOfRange));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 0, 5)),
+              StatusIs(StatusCode::kOutOfRange));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 3, dst_list, 0, 1)),
+              StatusIs(StatusCode::kOutOfRange));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 3, 1)),
+              StatusIs(StatusCode::kOutOfRange));
+
+  // Source has valid ranges, destination does not.
+  IREE_ASSERT_OK(iree_vm_list_resize(src_list, 4));
+  IREE_ASSERT_OK(iree_vm_list_resize(dst_list, 0));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 100, 1)),
+              StatusIs(StatusCode::kOutOfRange));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 0, 100)),
+              StatusIs(StatusCode::kOutOfRange));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 0, 5)),
+              StatusIs(StatusCode::kOutOfRange));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 3, dst_list, 0, 1)),
+              StatusIs(StatusCode::kOutOfRange));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 3, 1)),
+              StatusIs(StatusCode::kOutOfRange));
+
+  // Destination has valid ranges, source does not.
+  IREE_ASSERT_OK(iree_vm_list_resize(src_list, 0));
+  IREE_ASSERT_OK(iree_vm_list_resize(dst_list, 4));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 100, 1)),
+              StatusIs(StatusCode::kOutOfRange));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 0, 100)),
+              StatusIs(StatusCode::kOutOfRange));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 0, 5)),
+              StatusIs(StatusCode::kOutOfRange));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 3, dst_list, 0, 1)),
+              StatusIs(StatusCode::kOutOfRange));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 3, 1)),
+              StatusIs(StatusCode::kOutOfRange));
+
+  // Mismatches.
+  IREE_ASSERT_OK(iree_vm_list_resize(src_list, 4));
+  IREE_ASSERT_OK(iree_vm_list_resize(dst_list, 4));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 100, 1)),
+              StatusIs(StatusCode::kOutOfRange));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 0, 100)),
+              StatusIs(StatusCode::kOutOfRange));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 0, 5)),
+              StatusIs(StatusCode::kOutOfRange));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 4, dst_list, 0, 1)),
+              StatusIs(StatusCode::kOutOfRange));
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 4, 1)),
+              StatusIs(StatusCode::kOutOfRange));
+
+  iree_vm_list_release(src_list);
+  iree_vm_list_release(dst_list);
+}
+
+// Tests copying values between lists.
+TEST_F(VMListTest, CopyValues) {
+  iree_vm_type_def_t element_type =
+      iree_vm_type_def_make_value_type(IREE_VM_VALUE_TYPE_I32);
+
+  // src: [0, 1, 2, 3]
+  iree_vm_list_t* src_list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(&element_type, /*initial_capacity=*/8,
+                                     iree_allocator_system(), &src_list));
+  IREE_ASSERT_OK(iree_vm_list_resize(src_list, 4));
+  for (iree_host_size_t i = 0; i < 4; ++i) {
+    iree_vm_value_t value = iree_vm_value_make_i32(i);
+    IREE_ASSERT_OK(iree_vm_list_set_value(src_list, i, &value));
+  }
+
+  // dst: [4, 5, 6, 7]
+  iree_vm_list_t* dst_list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(&element_type, /*initial_capacity=*/8,
+                                     iree_allocator_system(), &dst_list));
+  IREE_ASSERT_OK(iree_vm_list_resize(dst_list, 4));
+  for (iree_host_size_t i = 0; i < 4; ++i) {
+    iree_vm_value_t value = iree_vm_value_make_i32(4 + i);
+    IREE_ASSERT_OK(iree_vm_list_set_value(dst_list, i, &value));
+  }
+
+  // Copy no items (no-op).
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, dst_list, 0, 0));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({4, 5, 6, 7})));
+
+  // Copy at start.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, dst_list, 0, 1));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 5, 6, 7})));
+
+  // Copy at end.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 3, dst_list, 3, 1));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 5, 6, 3})));
+
+  // Copy a range in the middle.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 1, dst_list, 1, 2));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 1, 2, 3})));
+
+  // Scatter.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, dst_list, 1, 3));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 0, 1, 2})));
+
+  // Try to copy over source - this should fail as we don't support memmove.
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, src_list, 1, 2)),
+              StatusIs(StatusCode::kInvalidArgument));
+
+  // But copying over non-overlapping source ranges should be ok.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, src_list, 2, 2));
+  EXPECT_THAT(GetValuesList(src_list), Eq(MakeValuesList({0, 1, 0, 1})));
+
+  iree_vm_list_release(src_list);
+  iree_vm_list_release(dst_list);
+}
+
+// Tests that trying to copy between values of different types will fail.
+// Note that we use sizeof(int) == sizeof(float) as even though the sizes match
+// we still want to fail (bitcasting would be bad).
+TEST_F(VMListTest, CopyWrongValues) {
+  // src: [0, 1, 2, 3]
+  iree_vm_type_def_t src_element_type =
+      iree_vm_type_def_make_value_type(IREE_VM_VALUE_TYPE_I32);
+  iree_vm_list_t* src_list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(&src_element_type, /*initial_capacity=*/8,
+                                     iree_allocator_system(), &src_list));
+  IREE_ASSERT_OK(iree_vm_list_resize(src_list, 4));
+  for (iree_host_size_t i = 0; i < 4; ++i) {
+    iree_vm_value_t value = iree_vm_value_make_i32(i);
+    IREE_ASSERT_OK(iree_vm_list_set_value(src_list, i, &value));
+  }
+
+  // dst: [4.0, 5.0, 6.0, 7.0]
+  iree_vm_type_def_t dst_element_type =
+      iree_vm_type_def_make_value_type(IREE_VM_VALUE_TYPE_F32);
+  iree_vm_list_t* dst_list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(&dst_element_type, /*initial_capacity=*/8,
+                                     iree_allocator_system(), &dst_list));
+  IREE_ASSERT_OK(iree_vm_list_resize(dst_list, 4));
+  for (iree_host_size_t i = 0; i < 4; ++i) {
+    iree_vm_value_t value = iree_vm_value_make_f32(4 + i);
+    IREE_ASSERT_OK(iree_vm_list_set_value(dst_list, i, &value));
+  }
+
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 0, 1)),
+              StatusIs(StatusCode::kInvalidArgument));
+
+  iree_vm_list_release(src_list);
+  iree_vm_list_release(dst_list);
+}
+
+// Tests copying refs between lists of !vm.ref<?>.
+TEST_F(VMListTest, CopyRefs) {
+  iree_vm_type_def_t element_type =
+      iree_vm_type_def_make_ref_type(IREE_VM_REF_TYPE_ANY);
+
+  // src: [0, 1, 2, 3]
+  iree_vm_list_t* src_list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(&element_type, /*initial_capacity=*/8,
+                                     iree_allocator_system(), &src_list));
+  iree_host_size_t src_list_size = 4;
+  for (iree_host_size_t i = 0; i < src_list_size; ++i) {
+    iree_vm_ref_t ref = MakeRef<B>(i);
+    IREE_ASSERT_OK(iree_vm_list_push_ref_move(src_list, &ref));
+  }
+
+  // dst: [4, 5, 6, 7]
+  iree_vm_list_t* dst_list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(&element_type, /*initial_capacity=*/8,
+                                     iree_allocator_system(), &dst_list));
+  iree_host_size_t dst_list_size = 4;
+  for (iree_host_size_t i = 0; i < dst_list_size; ++i) {
+    iree_vm_ref_t ref = MakeRef<B>(4 + i);
+    IREE_ASSERT_OK(iree_vm_list_push_ref_move(dst_list, &ref));
+  }
+
+  // Copy no items (no-op).
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, dst_list, 0, 0));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({4, 5, 6, 7})));
+
+  // Copy at start.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, dst_list, 0, 1));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 5, 6, 7})));
+
+  // Copy at end.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 3, dst_list, 3, 1));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 5, 6, 3})));
+
+  // Copy a range in the middle.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 1, dst_list, 1, 2));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 1, 2, 3})));
+
+  // Scatter.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, dst_list, 1, 3));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 0, 1, 2})));
+
+  // Try to copy over source - this should fail as we don't support memmove.
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, src_list, 1, 2)),
+              StatusIs(StatusCode::kInvalidArgument));
+
+  // But copying over non-overlapping source ranges should be ok.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, src_list, 2, 2));
+  EXPECT_THAT(GetValuesList(src_list), Eq(MakeValuesList({0, 1, 0, 1})));
+
+  iree_vm_list_release(src_list);
+  iree_vm_list_release(dst_list);
+}
+
+// Tests that trying to copy between refs of different types will fail.
+TEST_F(VMListTest, CopyWrongRefs) {
+  // src: type A
+  iree_vm_type_def_t src_element_type =
+      iree_vm_type_def_make_ref_type(test_a_type_id());
+  iree_vm_list_t* src_list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(&src_element_type, /*initial_capacity=*/8,
+                                     iree_allocator_system(), &src_list));
+  iree_host_size_t src_list_size = 4;
+  for (iree_host_size_t i = 0; i < src_list_size; ++i) {
+    iree_vm_ref_t ref = MakeRef<A>(i);
+    IREE_ASSERT_OK(iree_vm_list_push_ref_move(src_list, &ref));
+  }
+
+  // dst: type B
+  iree_vm_type_def_t dst_element_type =
+      iree_vm_type_def_make_ref_type(test_b_type_id());
+  iree_vm_list_t* dst_list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(&dst_element_type, /*initial_capacity=*/8,
+                                     iree_allocator_system(), &dst_list));
+  iree_host_size_t dst_list_size = 4;
+  for (iree_host_size_t i = 0; i < dst_list_size; ++i) {
+    iree_vm_ref_t ref = MakeRef<B>(4 + i);
+    IREE_ASSERT_OK(iree_vm_list_push_ref_move(dst_list, &ref));
+  }
+
+  // Copy no items (no-op). Should be ok.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, dst_list, 0, 0));
+
+  // Copies should fail because the types don't match.
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, dst_list, 0, 1)),
+              StatusIs(StatusCode::kInvalidArgument));
+  EXPECT_THAT(Status(iree_vm_list_copy(dst_list, 0, src_list, 0, 1)),
+              StatusIs(StatusCode::kInvalidArgument));
+
+  iree_vm_list_release(src_list);
+  iree_vm_list_release(dst_list);
+}
+
+// Tests copying between variant lists.
+TEST_F(VMListTest, CopyVariants) {
+  // src: [0, 1, B(2), B(3)]
+  iree_vm_list_t* src_list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(/*element_type=*/nullptr,
+                                     /*initial_capacity=*/8,
+                                     iree_allocator_system(), &src_list));
+  IREE_ASSERT_OK(iree_vm_list_resize(src_list, 4));
+  for (iree_host_size_t i = 0; i < 2; ++i) {
+    iree_vm_value_t value = iree_vm_value_make_i32(i);
+    IREE_ASSERT_OK(iree_vm_list_set_value(src_list, i, &value));
+  }
+  for (iree_host_size_t i = 2; i < 4; ++i) {
+    iree_vm_ref_t ref = MakeRef<B>(i);
+    IREE_ASSERT_OK(iree_vm_list_set_ref_move(src_list, i, &ref));
+  }
+
+  // dst: [4, 5, B(6), B(7)]
+  iree_vm_list_t* dst_list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(/*element_type=*/nullptr,
+                                     /*initial_capacity=*/8,
+                                     iree_allocator_system(), &dst_list));
+  IREE_ASSERT_OK(iree_vm_list_resize(dst_list, 4));
+  for (iree_host_size_t i = 0; i < 2; ++i) {
+    iree_vm_value_t value = iree_vm_value_make_i32(4 + i);
+    IREE_ASSERT_OK(iree_vm_list_set_value(dst_list, i, &value));
+  }
+  for (iree_host_size_t i = 2; i < 4; ++i) {
+    iree_vm_ref_t ref = MakeRef<B>(4 + i);
+    IREE_ASSERT_OK(iree_vm_list_set_ref_move(dst_list, i, &ref));
+  }
+
+  // Copy no items (no-op).
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, dst_list, 0, 0));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({4, 5, 6, 7})));
+
+  // Copy at start.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, dst_list, 0, 1));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 5, 6, 7})));
+
+  // Copy at end.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 3, dst_list, 3, 1));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 5, 6, 3})));
+
+  // Copy a range in the middle.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 1, dst_list, 1, 2));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 1, 2, 3})));
+
+  // Scatter.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, dst_list, 1, 3));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 0, 1, 2})));
+
+  // Try to copy over source - this should fail as we don't support memmove.
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, src_list, 1, 2)),
+              StatusIs(StatusCode::kInvalidArgument));
+
+  // But copying over non-overlapping source ranges should be ok.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, src_list, 2, 2));
+  EXPECT_THAT(GetValuesList(src_list), Eq(MakeValuesList({0, 1, 0, 1})));
+
+  iree_vm_list_release(src_list);
+  iree_vm_list_release(dst_list);
+}
+
+// Tests copying from variant lists to typed lists.
+TEST_F(VMListTest, CopyFromVariants) {
+  // src: [0, 1, B(2), B(3)]
+  iree_vm_list_t* src_list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(/*element_type=*/nullptr,
+                                     /*initial_capacity=*/8,
+                                     iree_allocator_system(), &src_list));
+  IREE_ASSERT_OK(iree_vm_list_resize(src_list, 4));
+  for (iree_host_size_t i = 0; i < 2; ++i) {
+    iree_vm_value_t value = iree_vm_value_make_i32(i);
+    IREE_ASSERT_OK(iree_vm_list_set_value(src_list, i, &value));
+  }
+  for (iree_host_size_t i = 2; i < 4; ++i) {
+    iree_vm_ref_t ref = MakeRef<B>(i);
+    IREE_ASSERT_OK(iree_vm_list_set_ref_move(src_list, i, &ref));
+  }
+
+  // dst: [4, 5, 6, 7]
+  iree_vm_type_def_t dst_element_type =
+      iree_vm_type_def_make_value_type(IREE_VM_VALUE_TYPE_I32);
+  iree_vm_list_t* dst_list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(&dst_element_type, /*initial_capacity=*/8,
+                                     iree_allocator_system(), &dst_list));
+  IREE_ASSERT_OK(iree_vm_list_resize(dst_list, 4));
+  for (iree_host_size_t i = 0; i < 4; ++i) {
+    iree_vm_value_t value = iree_vm_value_make_i32(4 + i);
+    IREE_ASSERT_OK(iree_vm_list_set_value(dst_list, i, &value));
+  }
+
+  // Copy no items (no-op).
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, dst_list, 0, 0));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({4, 5, 6, 7})));
+
+  // Copy at start.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, dst_list, 0, 1));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 5, 6, 7})));
+
+  // Copy at end; should fail because the elements are ref types.
+  // We check that nothing in the list changed.
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 3, dst_list, 3, 1)),
+              StatusIs(StatusCode::kInvalidArgument));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 5, 6, 7})));
+
+  // Copy a range in the middle including wrong types.
+  // We check that nothing in the list changed.
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 1, dst_list, 1, 2)),
+              StatusIs(StatusCode::kInvalidArgument));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 5, 6, 7})));
+
+  // Scatter.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, dst_list, 1, 2));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 0, 1, 7})));
+
+  // Try to copy over source - this should fail as we don't support memmove.
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, src_list, 1, 2)),
+              StatusIs(StatusCode::kInvalidArgument));
+
+  // But copying over non-overlapping source ranges should be ok.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, src_list, 2, 2));
+  EXPECT_THAT(GetValuesList(src_list), Eq(MakeValuesList({0, 1, 0, 1})));
+
+  iree_vm_list_release(src_list);
+  iree_vm_list_release(dst_list);
+}
+
+// Tests copying from typed lists to variant lists.
+TEST_F(VMListTest, CopyToVariants) {
+  // src: [0, 1, 2, 3]
+  iree_vm_type_def_t src_element_type =
+      iree_vm_type_def_make_value_type(IREE_VM_VALUE_TYPE_I32);
+  iree_vm_list_t* src_list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(&src_element_type, /*initial_capacity=*/8,
+                                     iree_allocator_system(), &src_list));
+  IREE_ASSERT_OK(iree_vm_list_resize(src_list, 4));
+  for (iree_host_size_t i = 0; i < 4; ++i) {
+    iree_vm_value_t value = iree_vm_value_make_i32(i);
+    IREE_ASSERT_OK(iree_vm_list_set_value(src_list, i, &value));
+  }
+
+  // dst: [4, 5, B(6), B(7)]
+  iree_vm_list_t* dst_list = nullptr;
+  IREE_ASSERT_OK(iree_vm_list_create(/*element_type=*/nullptr,
+                                     /*initial_capacity=*/8,
+                                     iree_allocator_system(), &dst_list));
+  IREE_ASSERT_OK(iree_vm_list_resize(dst_list, 4));
+  for (iree_host_size_t i = 0; i < 2; ++i) {
+    iree_vm_value_t value = iree_vm_value_make_i32(4 + i);
+    IREE_ASSERT_OK(iree_vm_list_set_value(dst_list, i, &value));
+  }
+  for (iree_host_size_t i = 2; i < 4; ++i) {
+    iree_vm_ref_t ref = MakeRef<B>(4 + i);
+    IREE_ASSERT_OK(iree_vm_list_set_ref_move(dst_list, i, &ref));
+  }
+
+  // Copy no items (no-op).
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, dst_list, 0, 0));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({4, 5, 6, 7})));
+
+  // Copy at start.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, dst_list, 0, 1));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 5, 6, 7})));
+
+  // Copy at end.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 3, dst_list, 3, 1));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 5, 6, 3})));
+
+  // Copy a range in the middle.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 1, dst_list, 1, 2));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 1, 2, 3})));
+
+  // Scatter.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, dst_list, 1, 3));
+  EXPECT_THAT(GetValuesList(dst_list), Eq(MakeValuesList({0, 0, 1, 2})));
+
+  // Try to copy over source - this should fail as we don't support memmove.
+  EXPECT_THAT(Status(iree_vm_list_copy(src_list, 0, src_list, 1, 2)),
+              StatusIs(StatusCode::kInvalidArgument));
+
+  // But copying over non-overlapping source ranges should be ok.
+  IREE_EXPECT_OK(iree_vm_list_copy(src_list, 0, src_list, 2, 2));
+  EXPECT_THAT(GetValuesList(src_list), Eq(MakeValuesList({0, 1, 0, 1})));
+
+  iree_vm_list_release(src_list);
+  iree_vm_list_release(dst_list);
+}
+
 // TODO(benvanik): test value get/set.
 
 // TODO(benvanik): test value conversion.
@@ -597,7 +1219,7 @@
   // Pops when empty fail.
   iree_vm_ref_t empty_ref{0};
   EXPECT_THAT(Status(iree_vm_list_pop_front_ref_move(list, &empty_ref)),
-              StatusIs(iree::StatusCode::kOutOfRange));
+              StatusIs(StatusCode::kOutOfRange));
 
   // Push back [0, 5).
   for (iree_host_size_t i = 0; i < 5; ++i) {
diff --git a/runtime/src/iree/vm/ref.c b/runtime/src/iree/vm/ref.c
index 64c7f16..5e4c822 100644
--- a/runtime/src/iree/vm/ref.c
+++ b/runtime/src/iree/vm/ref.c
@@ -187,7 +187,8 @@
 
 IREE_API_EXPORT iree_status_t iree_vm_ref_retain_checked(
     iree_vm_ref_t* ref, iree_vm_ref_type_t type, iree_vm_ref_t* out_ref) {
-  if (ref->type != IREE_VM_REF_TYPE_NULL && ref->type != type) {
+  if (ref->type != IREE_VM_REF_TYPE_NULL && ref->type != type &&
+      type != IREE_VM_REF_TYPE_ANY) {
     return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
                             "source ref type mismatch");
   }
@@ -207,7 +208,8 @@
 IREE_API_EXPORT iree_status_t iree_vm_ref_retain_or_move_checked(
     int is_move, iree_vm_ref_t* ref, iree_vm_ref_type_t type,
     iree_vm_ref_t* out_ref) {
-  if (ref->type != IREE_VM_REF_TYPE_NULL && ref->type != type) {
+  if (ref->type != IREE_VM_REF_TYPE_NULL && ref->type != type &&
+      type != IREE_VM_REF_TYPE_ANY) {
     // Make no changes on failure.
     return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
                             "source ref type mismatch");