Adding VM module dependency versioning support. (#10014)

Modules now export a version number and each vm.import can declare
a minimum required version. When serializing the module out the minimum
module dependencies are calculated and written to the flatbuffer/emit-c
descriptor.

We can now get nice runtime errors on module mismatches:
```
context.c:134: NOT_FOUND; required module 'hal' version mismatch; have 0 but require 1; resolving module 'module' imports
```

Future changes will use this information to perform module dependency
resolution/dynamic loading.

Fixes #8014.
diff --git a/runtime/bindings/python/py_module.cc b/runtime/bindings/python/py_module.cc
index a74aa16..37bf892 100644
--- a/runtime/bindings/python/py_module.cc
+++ b/runtime/bindings/python/py_module.cc
@@ -34,6 +34,8 @@
     interface_.destroy = &PyModuleInterface::ModuleDestroy;
     interface_.name = &PyModuleInterface::ModuleName;
     interface_.signature = &PyModuleInterface::ModuleSignature;
+    interface_.enumerate_dependencies =
+        &PyModuleInterface::ModuleEnumerateDependencies;
     interface_.get_function = &PyModuleInterface::ModuleGetFunction;
     interface_.lookup_function = &PyModuleInterface::ModuleLookupFunction;
     interface_.alloc_state = &PyModuleInterface::ModuleAllocState;
@@ -42,41 +44,50 @@
     interface_.notify = &PyModuleInterface::ModuleNotify;
     interface_.begin_call = &PyModuleInterface::ModuleBeginCall;
   }
-  PyModuleInterface(const PyModuleInterface &) = delete;
+  PyModuleInterface(const PyModuleInterface&) = delete;
   ~PyModuleInterface() = default;
 
-  static PyModuleInterface *AsSelf(void *vself) {
-    return static_cast<PyModuleInterface *>(vself);
+  static PyModuleInterface* AsSelf(void* vself) {
+    return static_cast<PyModuleInterface*>(vself);
   }
 
-  static void ModuleDestroy(void *vself) {
+  static void ModuleDestroy(void* vself) {
     auto self = AsSelf(vself);
     py::gil_scoped_acquire acquire;
     self->retained_self_ref_ = {};
   }
 
-  static iree_string_view_t ModuleName(void *vself) {
+  static iree_string_view_t ModuleName(void* vself) {
     auto self = AsSelf(vself);
     return {self->module_name_.data(), self->module_name_.size()};
   }
 
-  static iree_vm_module_signature_t ModuleSignature(void *vself) {
+  static iree_vm_module_signature_t ModuleSignature(void* vself) {
     auto self = AsSelf(vself);
     iree_vm_module_signature_t signature = {0};
-    signature.import_function_count = 0;
+    signature.version = self->descriptor_.version;
+    signature.attr_count = 0;
+    signature.import_function_count = self->imports_.size();
     signature.export_function_count = self->exports_.size();
     signature.internal_function_count = 0;
     return signature;
   }
 
+  static iree_status_t ModuleEnumerateDependencies(
+      void* vself, iree_vm_module_dependency_callback_t callback,
+      void* user_data) {
+    // TODO(laurenzo): python support for declaring dependencies on the module.
+    return iree_ok_status();
+  }
+
   static iree_status_t ModuleGetFunction(
-      void *vself, iree_vm_function_linkage_t linkage, iree_host_size_t ordinal,
-      iree_vm_function_t *out_function, iree_string_view_t *out_name,
-      iree_vm_function_signature_t *out_signature) {
+      void* vself, iree_vm_function_linkage_t linkage, iree_host_size_t ordinal,
+      iree_vm_function_t* out_function, iree_string_view_t* out_name,
+      iree_vm_function_signature_t* out_signature) {
     auto self = AsSelf(vself);
     if (IREE_LIKELY(linkage == IREE_VM_FUNCTION_LINKAGE_EXPORT)) {
       if (IREE_LIKELY(ordinal < self->export_functions_.size())) {
-        std::unique_ptr<PyFunction> &f = self->export_functions_[ordinal];
+        std::unique_ptr<PyFunction>& f = self->export_functions_[ordinal];
         if (IREE_LIKELY(out_function)) {
           out_function->linkage = linkage;
           out_function->module = &self->interface_;
@@ -95,10 +106,10 @@
     return iree_make_status(IREE_STATUS_NOT_FOUND);
   }
 
-  static iree_status_t ModuleLookupFunction(void *vself,
+  static iree_status_t ModuleLookupFunction(void* vself,
                                             iree_vm_function_linkage_t linkage,
                                             iree_string_view_t name,
-                                            iree_vm_function_t *out_function) {
+                                            iree_vm_function_t* out_function) {
     auto self = AsSelf(vself);
     std::string_view name_cpp(name.data, name.size);
     if (linkage == IREE_VM_FUNCTION_LINKAGE_EXPORT) {
@@ -115,8 +126,8 @@
   }
 
   static iree_status_t ModuleAllocState(
-      void *vself, iree_allocator_t allocator,
-      iree_vm_module_state_t **out_module_state) {
+      void* vself, iree_allocator_t allocator,
+      iree_vm_module_state_t** out_module_state) {
     auto self = AsSelf(vself);
     *out_module_state = nullptr;
     py::gil_scoped_acquire acquire;
@@ -125,40 +136,40 @@
       // Steal the reference and use the raw PyObject* as the state.
       // This will be released in ModuleFreeState.
       *out_module_state =
-          reinterpret_cast<iree_vm_module_state_t *>(py_state.release().ptr());
+          reinterpret_cast<iree_vm_module_state_t*>(py_state.release().ptr());
       return iree_ok_status();
-    } catch (std::exception &e) {
+    } catch (std::exception& e) {
       return iree_make_status(IREE_STATUS_UNKNOWN,
                               "Exception in call to PyModule constructor: %s",
                               e.what());
     }
   }
 
-  static void ModuleFreeState(void *vself,
-                              iree_vm_module_state_t *module_state) {
+  static void ModuleFreeState(void* vself,
+                              iree_vm_module_state_t* module_state) {
     py::gil_scoped_acquire acquire;
     // Release the reference stolen in ModuleAllocState.
     auto retained_handle =
-        py::handle(reinterpret_cast<PyObject *>(module_state));
+        py::handle(reinterpret_cast<PyObject*>(module_state));
     retained_handle.dec_ref();
   }
 
   static iree_status_t ModuleResolveImport(
-      void *vself, iree_vm_module_state_t *module_state,
-      iree_host_size_t ordinal, const iree_vm_function_t *function,
-      const iree_vm_function_signature_t *signature) {
+      void* vself, iree_vm_module_state_t* module_state,
+      iree_host_size_t ordinal, const iree_vm_function_t* function,
+      const iree_vm_function_signature_t* signature) {
     return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
                             "Python API does not support imports");
   }
 
-  static iree_status_t ModuleNotify(void *vself,
-                                    iree_vm_module_state_t *module_state,
+  static iree_status_t ModuleNotify(void* vself,
+                                    iree_vm_module_state_t* module_state,
                                     iree_vm_signal_t signal) {
     return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
                             "ModuleNotify not implemented");
   }
 
-  static iree_status_t ModuleBeginCall(void *vself, iree_vm_stack_t *stack,
+  static iree_status_t ModuleBeginCall(void* vself, iree_vm_stack_t* stack,
                                        iree_vm_function_call_t call) {
     auto self = AsSelf(vself);
     if (IREE_UNLIKELY(call.function.ordinal >=
@@ -169,18 +180,18 @@
                               self->export_functions_.size());
     }
 
-    auto &f = self->export_functions_[call.function.ordinal];
+    auto& f = self->export_functions_[call.function.ordinal];
     iree_host_size_t frame_size = 0;
-    iree_vm_stack_frame_t *callee_frame = nullptr;
+    iree_vm_stack_frame_t* callee_frame = nullptr;
     IREE_RETURN_IF_ERROR(iree_vm_stack_function_enter(
         stack, &call.function, IREE_VM_STACK_FRAME_NATIVE, frame_size,
         /*frame_cleanup_fn=*/nullptr, &callee_frame));
     auto state_object =
-        py::handle(reinterpret_cast<PyObject *>(callee_frame->module_state));
+        py::handle(reinterpret_cast<PyObject*>(callee_frame->module_state));
 
     try {
       IREE_RETURN_IF_ERROR(self->Invoke(*f, state_object, stack, call));
-    } catch (std::exception &e) {
+    } catch (std::exception& e) {
       return iree_make_status(IREE_STATUS_UNKNOWN,
                               "Exception raised from Python module: %s",
                               e.what());
@@ -228,13 +239,13 @@
     auto py_function = std::make_unique<PyFunction>(
         std::move(name), std::move(cconv), std::move(callable));
     exports_.push_back({});
-    iree_vm_native_export_descriptor_t &d = exports_.back();
+    iree_vm_native_export_descriptor_t& d = exports_.back();
     d.local_name = {py_function->name.data(), py_function->name.size()};
     d.calling_convention = {py_function->cconv.data(),
                             py_function->cconv.size()};
     d.attr_count = 0;
     d.attrs = nullptr;
-    std::string &alloced_name = py_function->name;
+    std::string& alloced_name = py_function->name;
     CheckApiStatus(py_function->ParseCconv(), "Unparseable calling convention");
 
     // Transfer the PyFunction to its vector now that we are done touching it.
@@ -251,9 +262,10 @@
     AssertMutable();
     initialized_ = true;
     memset(&descriptor_, 0, sizeof(descriptor_));
-    descriptor_.module_name = {module_name_.data(), module_name_.size()};
-    descriptor_.module_attr_count = attrs_.size();
-    descriptor_.module_attrs = attrs_.empty() ? nullptr : attrs_.data();
+    descriptor_.name = {module_name_.data(), module_name_.size()};
+    descriptor_.version = version_;
+    descriptor_.attr_count = attrs_.size();
+    descriptor_.attrs = attrs_.empty() ? nullptr : attrs_.data();
     descriptor_.import_count = imports_.size();
     descriptor_.imports = imports_.empty() ? nullptr : imports_.data();
     descriptor_.export_count = exports_.size();
@@ -302,10 +314,10 @@
     }
   };
 
-  iree_status_t Invoke(PyFunction &f, py::handle state_object,
-                       iree_vm_stack_t *stack, iree_vm_function_call_t call) {
+  iree_status_t Invoke(PyFunction& f, py::handle state_object,
+                       iree_vm_stack_t* stack, iree_vm_function_call_t call) {
     py::gil_scoped_acquire acquire;
-    uint8_t *packed_arguments = call.arguments.data;
+    uint8_t* packed_arguments = call.arguments.data;
     iree_host_size_t packed_arguments_required_size;
     // TODO: Is this validation needed or do we assume it from up-stack?
     IREE_RETURN_IF_ERROR(iree_vm_function_call_compute_cconv_fragment_size(
@@ -327,27 +339,27 @@
           break;
         case IREE_VM_CCONV_TYPE_I32:
           arguments.append(
-              py::cast(*reinterpret_cast<int32_t *>(packed_arguments)));
+              py::cast(*reinterpret_cast<int32_t*>(packed_arguments)));
           packed_arguments += sizeof(int32_t);
           break;
         case IREE_VM_CCONV_TYPE_F32:
           arguments.append(
-              py::cast(*reinterpret_cast<float *>(packed_arguments)));
+              py::cast(*reinterpret_cast<float*>(packed_arguments)));
           packed_arguments += sizeof(float);
           break;
         case IREE_VM_CCONV_TYPE_I64:
           arguments.append(
-              py::cast(*reinterpret_cast<int64_t *>(packed_arguments)));
+              py::cast(*reinterpret_cast<int64_t*>(packed_arguments)));
           packed_arguments += sizeof(int64_t);
           break;
         case IREE_VM_CCONV_TYPE_F64:
           arguments.append(
-              py::cast(*reinterpret_cast<double *>(packed_arguments)));
+              py::cast(*reinterpret_cast<double*>(packed_arguments)));
           packed_arguments += sizeof(double);
           break;
         case IREE_VM_CCONV_TYPE_REF: {
           iree_vm_ref_t ref =
-              *reinterpret_cast<iree_vm_ref_t *>(packed_arguments);
+              *reinterpret_cast<iree_vm_ref_t*>(packed_arguments);
           // Since the Python level VmRef can escape, it needs its own ref
           // count.
           VmRef py_ref;
@@ -370,40 +382,40 @@
     if (f.cconv_results.size == 0) {
       return iree_ok_status();
     }
-    uint8_t *packed_results = call.results.data;
+    uint8_t* packed_results = call.results.data;
     bool unary_result = f.cconv_results.size == 1;
-    auto pack_result = [&](py::object &value,
+    auto pack_result = [&](py::object& value,
                            char cconv_type) -> iree_status_t {
       switch (cconv_type) {
         case IREE_VM_CCONV_TYPE_VOID:
           break;
         case IREE_VM_CCONV_TYPE_I32:
-          *reinterpret_cast<int32_t *>(packed_results) =
+          *reinterpret_cast<int32_t*>(packed_results) =
               py::cast<int32_t>(value);
           packed_results += sizeof(int32_t);
           break;
         case IREE_VM_CCONV_TYPE_F32:
-          *reinterpret_cast<float *>(packed_results) = py::cast<float>(value);
+          *reinterpret_cast<float*>(packed_results) = py::cast<float>(value);
           packed_results += sizeof(float);
           break;
         case IREE_VM_CCONV_TYPE_I64:
-          *reinterpret_cast<int64_t *>(packed_results) =
+          *reinterpret_cast<int64_t*>(packed_results) =
               py::cast<int64_t>(value);
           packed_results += sizeof(int64_t);
           break;
         case IREE_VM_CCONV_TYPE_F64:
-          *reinterpret_cast<double *>(packed_results) = py::cast<double>(value);
+          *reinterpret_cast<double*>(packed_results) = py::cast<double>(value);
           packed_results += sizeof(double);
           break;
         case IREE_VM_CCONV_TYPE_REF: {
-          iree_vm_ref_t *result_ref =
-              reinterpret_cast<iree_vm_ref_t *>(packed_results);
+          iree_vm_ref_t* result_ref =
+              reinterpret_cast<iree_vm_ref_t*>(packed_results);
           if (value.is_none()) {
             return iree_make_status(
                 IREE_STATUS_FAILED_PRECONDITION,
                 "expected ref returned from Python function but got None");
           }
-          VmRef *py_ref = py::cast<VmRef *>(value);
+          VmRef* py_ref = py::cast<VmRef*>(value);
           iree_vm_ref_retain(&py_ref->ref(), result_ref);
           packed_results += sizeof(iree_vm_ref_t);
           break;
@@ -433,6 +445,7 @@
   // Descriptor state is built up when mutable and then will be populated
   // on the descriptor when frozen.
   std::string module_name_;
+  uint32_t version_;
   py::object ctor_;
   std::vector<iree_string_pair_t> attrs_;
   std::vector<iree_vm_native_import_descriptor_t> imports_;
@@ -452,7 +465,7 @@
   py::object retained_self_ref_;
 };
 
-void SetupPyModuleBindings(py::module &m) {
+void SetupPyModuleBindings(py::module& m) {
   py::class_<PyModuleInterface>(m, "PyModuleInterface")
       .def(py::init<std::string, py::object>(), py::arg("module_name"),
            py::arg("ctor"))
@@ -464,4 +477,4 @@
            py::arg("cconv"), py::arg("callable"));
 }
 
-}  // namespace iree::python
\ No newline at end of file
+}  // namespace iree::python
diff --git a/runtime/bindings/python/vm.cc b/runtime/bindings/python/vm.cc
index 349fa9e..c0b014e 100644
--- a/runtime/bindings/python/vm.cc
+++ b/runtime/bindings/python/vm.cc
@@ -624,6 +624,12 @@
   py::class_<VmModule>(m, "VmModule")
       .def_static("from_flatbuffer", &VmModule::FromFlatbufferBlob)
       .def_property_readonly("name", &VmModule::name)
+      .def_property_readonly("version",
+                             [](VmModule& self) {
+                               iree_vm_module_signature_t sig =
+                                   iree_vm_module_signature(self.raw_ptr());
+                               return sig.version;
+                             })
       .def("lookup_function", &VmModule::LookupFunction, py::arg("name"),
            py::arg("linkage") = IREE_VM_FUNCTION_LINKAGE_EXPORT)
       .def_property_readonly(