Repository: mun-lang/mun (language: rust)

Issue:
Replace `TypeInfo` with `TypeRef` in Runtime FFI
Currently, the host language is responsible for maintaining the `TypeInfo`'s memory when `insert_fn` is called, but using a `TypeRef` would allow us to avoid this.

Depends on #145


Discussion / hints:

Fix patch:
diff --git a/.clang-format b/.clang-format
index 4ca6bcb79..cc843b9d8 100644
--- a/.clang-format
+++ b/.clang-format
@@ -1,3 +1,5 @@
 BasedOnStyle: Google
 IndentWidth: 4
+IndentAccessModifiers: false
+AccessModifierOffset: -4
 ColumnLimit: 100
diff --git a/c/include/mun/abi.h b/c/include/mun/abi.h
index 533dcf9f9..ea739d3f0 100644
--- a/c/include/mun/abi.h
+++ b/c/include/mun/abi.h
@@ -42,6 +42,66 @@ typedef struct MunGuid {
     uint8_t _0[16];
 } MunGuid;
 
+/**
+ * Represents a unique identifier for types. The runtime can use this to lookup the corresponding [`TypeInfo`].
+ */
+typedef struct MunTypeId {
+    /**
+     * The GUID of the type
+     */
+    struct MunGuid guid;
+} MunTypeId;
+
+/**
+ * Represents a function signature.
+ */
+typedef struct MunFunctionSignature {
+    /**
+     * Argument types
+     */
+    const struct MunTypeId *arg_types;
+    /**
+     * Optional return type
+     */
+    struct MunTypeId return_type;
+    /**
+     * Number of argument types
+     */
+    uint16_t num_arg_types;
+} MunFunctionSignature;
+
+/**
+ * Represents a function prototype. A function prototype contains the name, type signature, but
+ * not an implementation.
+ */
+typedef struct MunFunctionPrototype {
+    /**
+     * Function name
+     */
+    const char *name;
+    /**
+     * The type signature of the function
+     */
+    struct MunFunctionSignature signature;
+} MunFunctionPrototype;
+
+/**
+ * Represents a function definition. A function definition contains the name, type signature, and
+ * a pointer to the implementation.
+ *
+ * `fn_ptr` can be used to call the declared function.
+ */
+typedef struct MunFunctionDefinition {
+    /**
+     * Function prototype
+     */
+    struct MunFunctionPrototype prototype;
+    /**
+     * Function pointer
+     */
+    const void *fn_ptr;
+} MunFunctionDefinition;
+
 /**
  * Represents a struct declaration.
  */
@@ -53,7 +113,7 @@ typedef struct MunStructInfo {
     /**
      * Struct fields' information
      */
-    const struct MunTypeInfo *const *field_types;
+    const struct MunTypeId *field_types;
     /**
      * Struct fields' offsets
      */
@@ -105,9 +165,9 @@ typedef union MunTypeInfoData {
  */
 typedef struct MunTypeInfo {
     /**
-     * Type GUID
+     * Type ID
      */
-    struct MunGuid guid;
+    struct MunTypeId id;
     /**
      * Type name
      */
@@ -126,56 +186,6 @@ typedef struct MunTypeInfo {
     union MunTypeInfoData data;
 } MunTypeInfo;
 
-/**
- * Represents a function signature.
- */
-typedef struct MunFunctionSignature {
-    /**
-     * Argument types
-     */
-    const struct MunTypeInfo *const *arg_types;
-    /**
-     * Optional return type
-     */
-    const struct MunTypeInfo *return_type;
-    /**
-     * Number of argument types
-     */
-    uint16_t num_arg_types;
-} MunFunctionSignature;
-
-/**
- * Represents a function prototype. A function prototype contains the name, type signature, but
- * not an implementation.
- */
-typedef struct MunFunctionPrototype {
-    /**
-     * Function name
-     */
-    const char *name;
-    /**
-     * The type signature of the function
-     */
-    struct MunFunctionSignature signature;
-} MunFunctionPrototype;
-
-/**
- * Represents a function definition. A function definition contains the name, type signature, and
- * a pointer to the implementation.
- *
- * `fn_ptr` can be used to call the declared function.
- */
-typedef struct MunFunctionDefinition {
-    /**
-     * Function prototype
-     */
-    struct MunFunctionPrototype prototype;
-    /**
-     * Function pointer
-     */
-    const void *fn_ptr;
-} MunFunctionDefinition;
-
 /**
  * Represents a module declaration.
  */
@@ -191,7 +201,7 @@ typedef struct MunModuleInfo {
     /**
      * Module types
      */
-    const struct MunTypeInfo *const *types;
+    const struct MunTypeInfo *types;
     /**
      * Number of module functions
      */
@@ -222,6 +232,30 @@ typedef struct MunDispatchTable {
     uint32_t num_entries;
 } MunDispatchTable;
 
+/**
+ * Represents a lookup table for type information. This is used for runtime linking.
+ *
+ * Type IDs and handles are stored separately for cache efficiency.
+ */
+typedef struct MunTypeLut {
+    /**
+     * Type IDs
+     */
+    const struct MunTypeId *type_ids;
+    /**
+     * Type information handles
+     */
+    const void **type_handles;
+    /**
+     * Debug names
+     */
+    const char *const *type_names;
+    /**
+     * Number of types
+     */
+    uint32_t num_entries;
+} MunTypeLut;
+
 /**
  * Represents an assembly declaration.
  */
@@ -231,9 +265,13 @@ typedef struct MunAssemblyInfo {
      */
     struct MunModuleInfo symbols;
     /**
-     * Dispatch table
+     * Function dispatch table
      */
     struct MunDispatchTable dispatch_table;
+    /**
+     * Type lookup table
+     */
+    struct MunTypeLut type_lut;
     /**
      * Paths to assembly dependencies
      */
diff --git a/cpp/include/mun/error.h b/cpp/include/mun/error.h
index 3071d7a44..3d683cd81 100644
--- a/cpp/include/mun/error.h
+++ b/cpp/include/mun/error.h
@@ -9,9 +9,9 @@ namespace mun {
  * Frees the corresponding error object on destruction, if it exists.
  */
 class Error {
-   public:
+public:
     /** Default constructs an error. */
-    Error() noexcept : m_handle{0} {}
+    Error() noexcept : m_handle{nullptr} {}
 
     /** Constructs an error from a `MunErrorHandle`.
      *
@@ -23,7 +23,7 @@ class Error {
      *
      * \param other an rvalue reference to an error
      */
-    Error(Error&& other) noexcept : m_handle(other.m_handle) { other.m_handle._0 = 0; }
+    Error(Error&& other) noexcept : m_handle(other.m_handle) { other.m_handle._0 = nullptr; }
 
     /** Move assigns an error.
      *
@@ -31,23 +31,23 @@ class Error {
      */
     Error& operator=(Error&& other) noexcept {
         m_handle = other.m_handle;
-        other.m_handle._0 = 0;
+        other.m_handle._0 = nullptr;
         return *this;
     }
 
     /** Destructs the error. */
     ~Error() noexcept { mun_error_destroy(m_handle); }
 
-    /** Retrieves the error message, if it exists, otherwise returns a nullptr.
+    /** Returns the error message, if it exists, otherwise returns a nullptr.
      *
      * The message is UTF-8 encoded.
      */
-    const char* message() noexcept { return mun_error_message(m_handle); }
+    const char* message() noexcept { return m_handle._0; }
 
-    /** Retrieves whether an error exists */
-    operator bool() const noexcept { return m_handle._0 != 0; }
+    /** Retrieves whether an error occurred */
+    operator bool() const noexcept { return m_handle._0 != nullptr; }
 
-   private:
+private:
     MunErrorHandle m_handle;
 };
 }  // namespace mun
diff --git a/cpp/include/mun/field_info.h b/cpp/include/mun/field_info.h
new file mode 100644
index 000000000..d8e67cc7e
--- /dev/null
+++ b/cpp/include/mun/field_info.h
@@ -0,0 +1,92 @@
+#ifndef MUN_FIELD_INFO_H_
+#define MUN_FIELD_INFO_H_
+
+#include <cassert>
+
+#include "mun/runtime_capi.h"
+#include "mun/type_info.h"
+
+namespace mun {
+/**
+ * @brief A wrapper around a Mun field information handle.
+ */
+class FieldInfo {
+public:
+    /**
+     * @brief Constructs field information from an instantiated `MunFieldInfoHandle`.
+     */
+    FieldInfo(MunFieldInfoHandle handle) noexcept : m_handle(handle) {}
+
+    /**
+     * @brief Retrieves the field's name.
+     */
+    std::string_view name() const noexcept {
+        const auto ptr = mun_field_info_name(m_handle);
+        assert(ptr);
+        return ptr;
+    }
+
+    /**
+     * @brief Retrieves the field's type.
+     */
+    TypeInfo type() const noexcept {
+        const auto type_handle = mun_field_info_type(m_handle);
+        assert(type_handle._0);
+        return TypeInfo(type_handle);
+    }
+
+    /**
+     * @brief Retrieves the field's offset.
+     */
+    uint16_t offset() const noexcept {
+        uint16_t offset;
+        const auto error_handle = mun_field_info_offset(m_handle, &offset);
+        assert(error_handle._0 == nullptr);
+        return offset;
+    }
+
+private:
+    MunFieldInfoHandle m_handle;
+};
+
+/**
+ * @brief A wrapper around a span of Mun field informations, which are owned by the Mun runtime.
+ */
+class FieldInfoSpan {
+public:
+    /**
+     * @brief Constructs a field information span from an instantiated `MunFieldInfoSpan`.
+     */
+    FieldInfoSpan(MunFieldInfoSpan span) noexcept : m_span(span) {}
+
+    FieldInfoSpan(FieldInfoSpan&&) = default;
+    FieldInfoSpan(const FieldInfoSpan&) = delete;
+
+    ~FieldInfoSpan() noexcept { mun_field_info_span_destroy(m_span); }
+
+    /**
+     * @brief Returns an iterator to the beginning.
+     */
+    const MunFieldInfoHandle* begin() const noexcept { return data(); }
+
+    /**
+     * @brief Returns an iterator to the end.
+     */
+    const MunFieldInfoHandle* end() const noexcept { return data() + size() + 1; }
+
+    /**
+     * @brief Returns a pointer to the beginning of the sequence of elements.
+     */
+    const MunFieldInfoHandle* data() const noexcept { return m_span.data; }
+
+    /**
+     * @brief Returns the number of elements in the sequence.
+     */
+    size_t size() const noexcept { return m_span.len; }
+
+private:
+    MunFieldInfoSpan m_span;
+};
+}  // namespace mun
+
+#endif  // MUN_FIELD_INFO_H_
diff --git a/cpp/include/mun/function_info.h b/cpp/include/mun/function_info.h
new file mode 100644
index 000000000..20a9282a9
--- /dev/null
+++ b/cpp/include/mun/function_info.h
@@ -0,0 +1,62 @@
+#ifndef MUN_FUNCTION_INFO_H_
+#define MUN_FUNCTION_INFO_H_
+
+#include <cassert>
+
+#include "mun/runtime_capi.h"
+#include "mun/type_info.h"
+
+namespace mun {
+/**
+ * @brief A wrapper around a Mun function information handle.
+ */
+class FunctionInfo {
+public:
+    /**
+     * @brief Constructs type information from an instantiated `MunFunctionInfoHandle`.
+     */
+    FunctionInfo(MunFunctionInfoHandle handle) noexcept : m_handle(handle) {}
+
+    /**
+     * @brief Retrieves the function's name.
+     */
+    std::string_view name() const noexcept {
+        const auto ptr = mun_function_info_name(m_handle);
+        assert(ptr);
+        return ptr;
+    }
+
+    /**
+     * @brief Retrieves the function's argument types.
+     */
+    TypeInfoSpan argument_types() const noexcept {
+        MunTypeInfoSpan span;
+        const auto error_handle = mun_function_info_argument_types(m_handle, &span);
+        assert(error_handle._0 == nullptr);
+        return TypeInfoSpan(span);
+    }
+
+    /**
+     * @brief Retrieves the function's return type.
+     */
+    TypeInfo return_type() const noexcept {
+        const auto handle = mun_function_info_return_type(m_handle);
+        assert(handle._0);
+        return TypeInfo(handle);
+    }
+
+    /**
+     * @brief Retrieves the function's pointer.
+     */
+    const void* function_pointer() const noexcept {
+        const void* fn_ptr = mun_function_info_fn_ptr(m_handle);
+        assert(fn_ptr);
+        return fn_ptr;
+    }
+
+private:
+    MunFunctionInfoHandle m_handle;
+};
+}  // namespace mun
+
+#endif  // MUN_FUNCTION_INFO_H_
diff --git a/cpp/include/mun/gc.h b/cpp/include/mun/gc.h
index f06fc661a..9cfc27b4f 100644
--- a/cpp/include/mun/gc.h
+++ b/cpp/include/mun/gc.h
@@ -9,7 +9,7 @@ namespace mun {
 class Runtime;
 
 class GcRootPtr {
-   public:
+public:
     /** Constructs a rooted garbage collection pointer from the provided raw
      * garbage collection handle.
      *
@@ -79,7 +79,7 @@ class GcRootPtr {
         return ptr;
     }
 
-   private:
+private:
     MunGcPtr m_ptr;
     const Runtime* m_runtime;
 };
diff --git a/cpp/include/mun/invoke_fn.h b/cpp/include/mun/invoke_fn.h
index 60f1e3776..98ab661b4 100644
--- a/cpp/include/mun/invoke_fn.h
+++ b/cpp/include/mun/invoke_fn.h
@@ -33,26 +33,25 @@ InvokeResult<Output, Args...> invoke_fn(Runtime& runtime, std::string_view fn_na
 
     Error error;
     constexpr auto NUM_ARGS = sizeof...(Args);
-    if (auto fn_info = runtime.find_function_definition(fn_name, &error); error) {
+    if (auto fn_info = runtime.find_function_info(fn_name, &error); error) {
         std::cerr << "Failed to retrieve function info due to error: " << error.message()
                   << std::endl;
     } else if (!fn_info) {
         std::cerr << "Failed to obtain function '" << fn_name << "'" << std::endl;
     } else {
-        const auto& prototype = fn_info->prototype;
-        const auto& signature = prototype.signature;
-        if (signature.num_arg_types != NUM_ARGS) {
+        const auto arg_types = fn_info->argument_types();
+        if (arg_types.size() != NUM_ARGS) {
             std::cerr << "Invalid number of arguments. Expected: "
-                      << std::to_string(signature.num_arg_types)
-                      << ". Found: " << std::to_string(NUM_ARGS) << "." << std::endl;
+                      << std::to_string(arg_types.size()) << ". Found: " << std::to_string(NUM_ARGS)
+                      << "." << std::endl;
 
             return make_error(runtime, fn_name, args...);
         }
 
         if constexpr (NUM_ARGS > 0) {
-            const MunTypeInfo* const* arg_ptr = signature.arg_types;
+            const MunTypeInfoHandle* arg_ptr = arg_types.begin();
             const std::optional<std::pair<const char*, const char*>> return_type_diffs[] = {
-                reflection::equals_argument_type(**(arg_ptr++), args)...};
+                reflection::equals_argument_type(TypeInfo(*(arg_ptr++)), args)...};
 
             for (size_t idx = 0; idx < NUM_ARGS; ++idx) {
                 if (auto diff = return_type_diffs[idx]) {
@@ -66,26 +65,16 @@ InvokeResult<Output, Args...> invoke_fn(Runtime& runtime, std::string_view fn_na
             }
         }
 
-        if (signature.return_type) {
-            const auto& return_type = signature.return_type;
-            if (auto diff = reflection::equals_return_type<Output>(*return_type)) {
-                const auto& [expected, found] = *diff;
-                std::cerr << "Invalid return type. Expected: " << expected << ". Found: " << found
-                          << "." << std::endl;
-
-                return make_error(runtime, fn_name, args...);
-            }
-        } else if (!reflection::equal_types<void, Output>()) {
-            std::cerr << "Invalid return type. Expected: "
-                      << ReturnTypeReflection<void>::type_name()
-                      << ". Found: " << ReturnTypeReflection<Output>::type_name() << "."
-                      << std::endl;
+        if (auto diff = reflection::equals_return_type<Output>(fn_info->return_type())) {
+            const auto& [expected, found] = *diff;
+            std::cerr << "Invalid return type. Expected: " << expected << ". Found: " << found
+                      << "." << std::endl;
 
             return make_error(runtime, fn_name, args...);
         }
 
         auto fn = reinterpret_cast<typename Marshal<Output>::type(MUN_CALLTYPE*)(
-            typename Marshal<Args>::type...)>(const_cast<void*>(fn_info->fn_ptr));
+            typename Marshal<Args>::type...)>(const_cast<void*>(fn_info->function_pointer()));
         if constexpr (std::is_same_v<Output, void>) {
             fn(Marshal<Args>::to(args)...);
             return InvokeResult<Output, Args...>(std::monostate{});
diff --git a/cpp/include/mun/marshal.h b/cpp/include/mun/marshal.h
index dc895900f..cf67db381 100644
--- a/cpp/include/mun/marshal.h
+++ b/cpp/include/mun/marshal.h
@@ -7,29 +7,27 @@ namespace mun {
 template <typename T>
 struct Marshal;
 
-#define IMPL_PRIMITIVE_TYPE_MARSHAL(ty)                                                          \
-    template <>                                                                                  \
-    struct Marshal<ty> {                                                                         \
-        using type = ty;                                                                         \
-                                                                                                 \
-        static type from(type value, const Runtime&) noexcept { return value; }                  \
-                                                                                                 \
-        static type to(type value) noexcept { return value; }                                    \
-                                                                                                 \
-        static type copy_from(const type* value, const Runtime&,                                 \
-                              std::optional<const MunTypeInfo*>) noexcept {                      \
-            return *value;                                                                       \
-        }                                                                                        \
-                                                                                                 \
-        static void move_to(type value, type* ptr, std::optional<const MunTypeInfo*>) noexcept { \
-            *ptr = std::move(value);                                                             \
-        }                                                                                        \
-                                                                                                 \
-        static type swap_at(type value, type* ptr, const Runtime&,                               \
-                            std::optional<const MunTypeInfo*>) noexcept {                        \
-            std::swap(value, *ptr);                                                              \
-            return std::move(value);                                                             \
-        }                                                                                        \
+#define IMPL_PRIMITIVE_TYPE_MARSHAL(ty)                                                            \
+    template <>                                                                                    \
+    struct Marshal<ty> {                                                                           \
+        using type = ty;                                                                           \
+                                                                                                   \
+        static type from(type value, const Runtime&) noexcept { return value; }                    \
+                                                                                                   \
+        static type to(type value) noexcept { return value; }                                      \
+                                                                                                   \
+        static type copy_from(const type* value, const Runtime&, const TypeInfo&) noexcept {       \
+            return *value;                                                                         \
+        }                                                                                          \
+                                                                                                   \
+        static void move_to(type value, type* ptr, const TypeInfo&) noexcept {                     \
+            *ptr = std::move(value);                                                               \
+        }                                                                                          \
+                                                                                                   \
+        static type swap_at(type value, type* ptr, const Runtime&, const TypeInfo&) noexcept {     \
+            std::swap(value, *ptr);                                                                \
+            return std::move(value);                                                               \
+        }                                                                                          \
     };
 
 // TODO: Add support for 128-bit integers
diff --git a/cpp/include/mun/reflection.h b/cpp/include/mun/reflection.h
index 246250425..60956a371 100644
--- a/cpp/include/mun/reflection.h
+++ b/cpp/include/mun/reflection.h
@@ -10,57 +10,51 @@
 #include "mun/type_info.h"
 
 namespace mun {
-constexpr inline bool operator==(const MunGuid& lhs, const MunGuid& rhs) noexcept {
+constexpr inline bool operator==(const MunTypeId& lhs, const MunTypeId& rhs) noexcept {
     for (auto idx = 0; idx < 16; ++idx) {
-        if (lhs._0[idx] != rhs._0[idx]) {
+        if (lhs.guid._0[idx] != rhs.guid._0[idx]) {
             return false;
         }
     }
     return true;
 }
 
-constexpr inline bool operator!=(const MunGuid& lhs, const MunGuid& rhs) noexcept {
+constexpr inline bool operator!=(const MunTypeId& lhs, const MunTypeId& rhs) noexcept {
     return !(lhs == rhs);
 }
 
 template <typename T>
 struct ArgumentReflection {
-    static constexpr const char* type_name(const T&) noexcept { return TypeInfo<T>::Type.name; }
-    static constexpr MunGuid type_guid(const T&) noexcept { return TypeInfo<T>::Type.guid; }
+    static constexpr const char* type_name(const T&) noexcept { return StaticTypeInfo<T>::name(); }
+    static constexpr MunTypeId type_id(const T&) noexcept { return StaticTypeInfo<T>::id(); }
 };
 
 template <typename T>
 struct ReturnTypeReflection {
-    static constexpr const char* type_name() noexcept { return TypeInfo<T>::Type.name; }
-    static constexpr MunGuid type_guid() noexcept { return TypeInfo<T>::Type.guid; }
-};
-
-template <>
-struct ReturnTypeReflection<void> {
-    static constexpr const char* type_name() noexcept { return "core::empty"; }
-    static constexpr MunGuid type_guid() noexcept { return details::type_guid(type_name()); }
+    static constexpr const char* type_name() noexcept { return StaticTypeInfo<T>::name(); }
+    static constexpr MunTypeId type_id() noexcept { return StaticTypeInfo<T>::id(); }
 };
 
 namespace reflection {
 template <typename T, typename U>
 constexpr bool equal_types() noexcept {
-    return ReturnTypeReflection<T>::type_guid() == ReturnTypeReflection<U>::type_guid();
+    return ReturnTypeReflection<T>::type_id() == ReturnTypeReflection<U>::type_id();
 }
 
 template <typename Arg>
 inline std::optional<std::pair<const char*, const char*>> equals_argument_type(
-    const MunTypeInfo& type_info, const Arg& arg) noexcept {
-    if (type_info.guid == ArgumentReflection<Arg>::type_guid(arg)) {
+    const TypeInfo& type_info, const Arg& arg) noexcept {
+    if (type_info.id() == ArgumentReflection<Arg>::type_id(arg)) {
         return std::nullopt;
     } else {
         const auto expected_name = ArgumentReflection<Arg>::type_name(arg);
-        return std::make_pair(type_info.name, expected_name);
+        return std::make_pair(type_info.name().data(), expected_name);
     }
 }
 
 template <typename T>
 inline std::optional<std::pair<const char*, const char*>> equals_return_type(
-    const MunTypeInfo& type_info) noexcept;
+    const TypeInfo& type_info) noexcept;
 
 }  // namespace reflection
 
@@ -72,13 +66,13 @@ namespace mun {
 namespace reflection {
 template <typename T>
 std::optional<std::pair<const char*, const char*>> equals_return_type(
-    const MunTypeInfo& type_info) noexcept {
-    if (type_info.data.tag == MunTypeInfoData_Tag::Primitive) {
-        if (type_info.guid != ReturnTypeReflection<T>::type_guid()) {
-            return std::make_pair(type_info.name, ReturnTypeReflection<T>::type_name());
+    const TypeInfo& type_info) noexcept {
+    if (type_info.data().tag == MunTypeInfoData_Tag::Primitive) {
+        if (type_info.id() != ReturnTypeReflection<T>::type_id()) {
+            return std::make_pair(type_info.name().data(), ReturnTypeReflection<T>::type_name());
         }
     } else if (!reflection::equal_types<StructRef, T>()) {
-        return std::make_pair(type_info.name, ReturnTypeReflection<T>::type_name());
+        return std::make_pair(type_info.name().data(), ReturnTypeReflection<T>::type_name());
     }
 
     return std::nullopt;
diff --git a/cpp/include/mun/runtime.h b/cpp/include/mun/runtime.h
index 66d7d1e9a..9414ffcf7 100644
--- a/cpp/include/mun/runtime.h
+++ b/cpp/include/mun/runtime.h
@@ -6,8 +6,10 @@
 #include <string_view>
 
 #include "mun/error.h"
-#include "mun/function.h"
+#include "mun/function_info.h"
 #include "mun/runtime_capi.h"
+#include "mun/runtime_function.h"
+#include "mun/type_info.h"
 
 namespace mun {
 
@@ -28,7 +30,7 @@ class Runtime {
      */
     Runtime(MunRuntimeHandle handle) noexcept : m_handle(handle) {}
 
-   public:
+public:
     /** Move constructs a runtime
      *
      * \param other an rvalue reference to a runtime
@@ -45,19 +47,19 @@ class Runtime {
      * \param out_error a pointer that will optionally return an error
      * \return possibly, the desired `MunFunctionDefinition` struct
      */
-    std::optional<MunFunctionDefinition> find_function_definition(
-        std::string_view fn_name, Error* out_error = nullptr) noexcept {
+    std::optional<FunctionInfo> find_function_info(std::string_view fn_name,
+                                                   Error* out_error = nullptr) noexcept {
         bool has_fn;
-        MunFunctionDefinition temp;
-        if (auto error = Error(
-                mun_runtime_get_function_definition(m_handle, fn_name.data(), &has_fn, &temp))) {
+        MunFunctionInfoHandle fn_info;
+        if (auto error =
+                Error(mun_runtime_get_function_info(m_handle, fn_name.data(), &has_fn, &fn_info))) {
             if (out_error) {
                 *out_error = std::move(error);
             }
             return std::nullopt;
         }
 
-        return has_fn ? std::make_optional(std::move(temp)) : std::nullopt;
+        return has_fn ? std::make_optional(std::move(fn_info)) : std::nullopt;
     }
 
     /**
@@ -69,10 +71,10 @@ class Runtime {
      * \param out_error a pointer to fill with a potential error
      * \return potentially, the handle of an allocated object
      */
-    std::optional<MunGcPtr> gc_alloc(MunUnsafeTypeInfo type_info,
+    std::optional<MunGcPtr> gc_alloc(const TypeInfo& type_info,
                                      Error* out_error = nullptr) const noexcept {
         MunGcPtr obj;
-        if (auto error = Error(mun_gc_alloc(m_handle, type_info, &obj))) {
+        if (auto error = Error(mun_gc_alloc(m_handle, type_info.handle(), &obj))) {
             if (out_error) {
                 *out_error = std::move(error);
             }
@@ -90,7 +92,7 @@ class Runtime {
     bool gc_collect() const noexcept {
         bool reclaimed;
         auto error_handle = mun_gc_collect(m_handle, &reclaimed);
-        assert(error_handle._0 == 0);
+        assert(error_handle._0 == nullptr);
 
         return reclaimed;
     }
@@ -109,7 +111,7 @@ class Runtime {
      */
     void gc_root_ptr(MunGcPtr obj) const noexcept {
         const auto error_handle = mun_gc_root(m_handle, obj);
-        assert(error_handle._0 == 0);
+        assert(error_handle._0 == nullptr);
     }
 
     /**
@@ -124,7 +126,7 @@ class Runtime {
      */
     void gc_unroot_ptr(MunGcPtr obj) const noexcept {
         const auto error_handle = mun_gc_unroot(m_handle, obj);
-        assert(error_handle._0 == 0);
+        assert(error_handle._0 == nullptr);
     }
 
     /**
@@ -133,11 +135,11 @@ class Runtime {
      * \param obj a garbage collection handle
      * \return the handle's type information
      */
-    MunUnsafeTypeInfo ptr_type(MunGcPtr obj) const noexcept {
-        MunUnsafeTypeInfo type_info;
-        const auto error_handle = mun_gc_ptr_type(m_handle, obj, &type_info);
-        assert(error_handle._0 == 0);
-        return type_info;
+    TypeInfo ptr_type(MunGcPtr obj) const noexcept {
+        MunTypeInfoHandle type_handle;
+        const auto error_handle = mun_gc_ptr_type(m_handle, obj, &type_handle);
+        assert(error_handle._0 == nullptr);
+        return TypeInfo(type_handle);
     }
 
     /** Checks for updates to hot reloadable assemblies.
@@ -156,7 +158,7 @@ class Runtime {
         return updated;
     }
 
-   private:
+private:
     MunRuntimeHandle m_handle;
 };
 
@@ -193,8 +195,7 @@ inline std::optional<Runtime> make_runtime(std::string_view library_path,
         definition = MunFunctionDefinition{
             MunFunctionPrototype{
                 func.name.c_str(),
-                MunFunctionSignature{func.arg_types.data(),
-                                     func.ret_type.has_value() ? func.ret_type.value() : nullptr,
+                MunFunctionSignature{func.arg_types.data(), func.ret_type,
                                      static_cast<uint16_t>(func.arg_types.size())}},
             func.fn_ptr};
     }
diff --git a/cpp/include/mun/runtime_capi.h b/cpp/include/mun/runtime_capi.h
index f72918429..53ad2b676 100644
--- a/cpp/include/mun/runtime_capi.h
+++ b/cpp/include/mun/runtime_capi.h
@@ -31,105 +31,61 @@ enum MunStructMemoryKind
 typedef uint8_t MunStructMemoryKind;
 #endif // __cplusplus
 
-typedef uintptr_t MunToken;
-
 /**
- * A C-style handle to an error.
+ * A C-style handle to an error message.
+ *
+ * If the handle contains a non-null pointer, an error occurred.
  */
 typedef struct MunErrorHandle {
-    MunToken _0;
+    const char *_0;
 } MunErrorHandle;
 
 /**
- * Represents a globally unique identifier (GUID).
+ * A C-style handle to a runtime.
  */
-typedef struct MunGuid {
-    uint8_t _0[16];
-} MunGuid;
+typedef struct MunRuntimeHandle {
+    void *_0;
+} MunRuntimeHandle;
 
 /**
- * Represents a struct declaration.
+ * A C-style handle to a `TypeInfo`.
  */
-typedef struct MunStructInfo {
-    /**
-     * Struct fields' names
-     */
-    const char *const *field_names;
-    /**
-     * Struct fields' information
-     */
-    const struct MunTypeInfo *const *field_types;
-    /**
-     * Struct fields' offsets
-     */
-    const uint16_t *field_offsets;
-    /**
-     * Number of fields
-     */
-    uint16_t num_fields;
-    /**
-     * Struct memory kind
-     */
-    MunStructMemoryKind memory_kind;
-} MunStructInfo;
+typedef struct MunTypeInfoHandle {
+    const void *_0;
+} MunTypeInfoHandle;
 
 /**
- * Contains data specific to a group of types that illicit the same characteristics.
+ * A `RawGcPtr` is an unsafe version of a `GcPtr`. It represents the raw internal pointer
+ * semantics used by the runtime.
  */
-enum MunTypeInfoData_Tag
-#ifdef __cplusplus
-  : uint8_t
-#endif // __cplusplus
- {
-    /**
-     * Primitive types (i.e. `()`, `bool`, `float`, `int`, etc.)
-     */
-    Primitive,
-    /**
-     * Struct types (i.e. record, tuple, or unit structs)
-     */
-    Struct,
-};
-#ifndef __cplusplus
-typedef uint8_t MunTypeInfoData_Tag;
-#endif // __cplusplus
-
-typedef union MunTypeInfoData {
-    MunTypeInfoData_Tag tag;
-    struct {
-        MunTypeInfoData_Tag struct_tag;
-        struct MunStructInfo struct_;
-    };
-} MunTypeInfoData;
+typedef void *const *MunRawGcPtr;
 
 /**
- * Represents the type declaration for a value type.
+ * A `GcPtr` is what you interact with outside of the allocator. It is a pointer to a piece of
+ * memory that points to the actual data stored in memory.
  *
- * TODO: add support for polymorphism, enumerations, type parameters, generic type definitions, and
- * constructed generic types.
+ * This creates an indirection that must be followed to get to the actual data of the object. Note
+ * that the `GcPtr` must therefore be pinned in memory whereas the contained memory pointer may
+ * change.
  */
-typedef struct MunTypeInfo {
+typedef MunRawGcPtr MunGcPtr;
+
+/**
+ * Represents a globally unique identifier (GUID).
+ */
+typedef struct MunGuid {
+    uint8_t _0[16];
+} MunGuid;
+
+/**
+ * Represents a unique identifier for types. The runtime can use this to lookup the corresponding [`TypeInfo`].
+ */
+typedef struct MunTypeId {
     /**
-     * Type GUID
+     * The GUID of the type
      */
     struct MunGuid guid;
-    /**
-     * Type name
-     */
-    const char *name;
-    /**
-     * The exact size of the type in bits without any padding
-     */
-    uint32_t size_in_bits;
-    /**
-     * The alignment of the type
-     */
-    uint8_t alignment;
-    /**
-     * Type group
-     */
-    union MunTypeInfoData data;
-} MunTypeInfo;
+} MunTypeId;
 
 /**
  * Represents a function signature.
@@ -138,11 +94,11 @@ typedef struct MunFunctionSignature {
     /**
      * Argument types
      */
-    const struct MunTypeInfo *const *arg_types;
+    const struct MunTypeId *arg_types;
     /**
      * Optional return type
      */
-    const struct MunTypeInfo *return_type;
+    struct MunTypeId return_type;
     /**
      * Number of argument types
      */
@@ -205,70 +161,171 @@ typedef struct MunRuntimeOptions {
 } MunRuntimeOptions;
 
 /**
- * A C-style handle to a runtime.
+ * A C-style handle to a `FunctionInfo`.
  */
-typedef struct MunRuntimeHandle {
-    void *_0;
-} MunRuntimeHandle;
+typedef struct MunFunctionInfoHandle {
+    const void *_0;
+} MunFunctionInfoHandle;
 
 /**
- * `UnsafeTypeInfo` is a type that wraps a `NonNull<TypeInfo>` and indicates unsafe interior
- * operations on the wrapped `TypeInfo`. The unsafety originates from uncertainty about the
- * lifetime of the wrapped `TypeInfo`.
- *
- * Rust lifetime rules do not allow separate lifetimes for struct fields, but we can make `unsafe`
- * guarantees about their lifetimes. Thus the `UnsafeTypeInfo` type is the only legal way to obtain
- * shared references to the wrapped `TypeInfo`.
+ * A C-style handle to a `FieldInfo`.
  */
-typedef struct MunTypeInfo *MunUnsafeTypeInfo;
+typedef struct MunFieldInfoHandle {
+    const void *_0;
+} MunFieldInfoHandle;
 
 /**
- * A `RawGcPtr` is an unsafe version of a `GcPtr`. It represents the raw internal pointer
- * semantics used by the runtime.
+ * A C-style handle to an array of `FieldInfoHandle`s.
  */
-typedef void *const *MunRawGcPtr;
+typedef struct MunFieldInfoSpan {
+    /**
+     * Pointer to the start of the array buffer
+     */
+    const struct MunFieldInfoHandle *data;
+    /**
+     * Length of the array (and capacity)
+     */
+    uintptr_t len;
+} MunFieldInfoSpan;
 
 /**
- * A `GcPtr` is what you interact with outside of the allocator. It is a pointer to a piece of
- * memory that points to the actual data stored in memory.
- *
- * This creates an indirection that must be followed to get to the actual data of the object. Note
- * that the `GcPtr` must therefore be pinned in memory whereas the contained memory pointer may
- * change.
+ * A C-style handle to an array of `TypeInfoHandle`s.
  */
-typedef MunRawGcPtr MunGcPtr;
+typedef struct MunTypeInfoSpan {
+    /**
+     * Pointer to the start of the array buffer
+     */
+    const struct MunTypeInfoHandle *data;
+    /**
+     * Length of the array (and capacity)
+     */
+    uintptr_t len;
+} MunTypeInfoSpan;
+
+/**
+ * A C-style handle to a `StructInfo`.
+ */
+typedef struct MunStructInfoHandle {
+    const void *_0;
+} MunStructInfoHandle;
+
+/**
+ * An enum containing C-style handles a `TypeInfo`'s data.
+ */
+enum MunTypeInfoData_Tag
+#ifdef __cplusplus
+  : uint8_t
+#endif // __cplusplus
+ {
+    /**
+     * Primitive types (i.e. `()`, `bool`, `float`, `int`, etc.)
+     */
+    Primitive,
+    /**
+     * Struct types (i.e. record, tuple, or unit structs)
+     */
+    Struct,
+};
+#ifndef __cplusplus
+typedef uint8_t MunTypeInfoData_Tag;
+#endif // __cplusplus
+
+typedef union MunTypeInfoData {
+    MunTypeInfoData_Tag tag;
+    struct {
+        MunTypeInfoData_Tag struct_tag;
+        struct MunStructInfoHandle struct_;
+    };
+} MunTypeInfoData;
+
+/**
+ * Represents a struct declaration.
+ */
+typedef struct MunStructInfo {
+    /**
+     * Struct fields' names
+     */
+    const char *const *field_names;
+    /**
+     * Struct fields' information
+     */
+    const struct MunTypeId *field_types;
+    /**
+     * Struct fields' offsets
+     */
+    const uint16_t *field_offsets;
+    /**
+     * Number of fields
+     */
+    uint16_t num_fields;
+    /**
+     * Struct memory kind
+     */
+    MunStructMemoryKind memory_kind;
+} MunStructInfo;
 
 #ifdef __cplusplus
 extern "C" {
 #endif // __cplusplus
 
 /**
- * Constructs a new runtime that loads the library at `library_path` and its dependencies. If
- * successful, the runtime `handle` is set, otherwise a non-zero error handle is returned.
+ * Deallocates a string that was allocated by the runtime.
+ *
+ * # Safety
+ *
+ * This function receives a raw pointer as parameter. Only when the argument is not a null pointer,
+ * its content will be deallocated. Passing pointers to invalid data or memory allocated by other
+ * processes, will lead to undefined behavior.
+ */
+void mun_string_destroy(const char *string);
+
+/**
+ * Destructs the error message corresponding to the specified handle.
+ *
+ * # Safety
+ *
+ * Only call this function on an ErrorHandle once.
+ */
+void mun_error_destroy(struct MunErrorHandle error);
+
+/**
+ * Allocates an object in the runtime of the given `type_info`. If successful, `obj` is set,
+ * otherwise a non-zero error handle is returned.
  *
  * If a non-zero error handle is returned, it must be manually destructed using
  * [`mun_error_destroy`].
  *
- * The runtime must be manually destructed using [`mun_runtime_destroy`].
- *
  * # Safety
  *
  * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
  * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-struct MunErrorHandle mun_runtime_create(const char *library_path,
-                                         struct MunRuntimeOptions options,
-                                         struct MunRuntimeHandle *handle);
+struct MunErrorHandle mun_gc_alloc(struct MunRuntimeHandle runtime,
+                                   struct MunTypeInfoHandle type_info,
+                                   MunGcPtr *obj);
 
 /**
- * Destructs the runtime corresponding to `handle`.
+ * Retrieves the `type_info` for the specified `obj` from the runtime. If successful, `type_info`
+ * is set, otherwise a non-zero error handle is returned.
+ *
+ * If a non-zero error handle is returned, it must be manually destructed using
+ * [`mun_error_destroy`].
+ *
+ * # Safety
+ *
+ * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
+ * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-void mun_runtime_destroy(struct MunRuntimeHandle handle);
+struct MunErrorHandle mun_gc_ptr_type(struct MunRuntimeHandle handle,
+                                      MunGcPtr obj,
+                                      struct MunTypeInfoHandle *type_info);
 
 /**
- * Retrieves the [`FunctionDefinition`] for `fn_name` from the runtime corresponding to `handle`.
- * If successful, `has_fn_info` and `fn_info` are set, otherwise a non-zero error handle is
- * returned.
+ * Roots the specified `obj`, which keeps it and objects it references alive. Objects marked as
+ * root, must call `mun_gc_unroot` before they can be collected. An object can be rooted multiple
+ * times, but you must make sure to call `mun_gc_unroot` an equal number of times before the
+ * object can be collected. If successful, `obj` has been rooted, otherwise a non-zero error handle
+ * is returned.
  *
  * If a non-zero error handle is returned, it must be manually destructed using
  * [`mun_error_destroy`].
@@ -278,14 +335,13 @@ void mun_runtime_destroy(struct MunRuntimeHandle handle);
  * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
  * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-struct MunErrorHandle mun_runtime_get_function_definition(struct MunRuntimeHandle handle,
-                                                          const char *fn_name,
-                                                          bool *has_fn_info,
-                                                          struct MunFunctionDefinition *fn_definition);
+struct MunErrorHandle mun_gc_root(struct MunRuntimeHandle handle, MunGcPtr obj);
 
 /**
- * Updates the runtime corresponding to `handle`. If successful, `updated` is set, otherwise a
- * non-zero error handle is returned.
+ * Unroots the specified `obj`, potentially allowing it and objects it references to be
+ * collected. An object can be rooted multiple times, so you must make sure to call `mun_gc_unroot`
+ * the same number of times as `mun_gc_root` was called before the object can be collected. If
+ * successful, `obj` has been unrooted, otherwise a non-zero error handle is returned.
  *
  * If a non-zero error handle is returned, it must be manually destructed using
  * [`mun_error_destroy`].
@@ -295,33 +351,50 @@ struct MunErrorHandle mun_runtime_get_function_definition(struct MunRuntimeHandl
  * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
  * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-struct MunErrorHandle mun_runtime_update(struct MunRuntimeHandle handle, bool *updated);
+struct MunErrorHandle mun_gc_unroot(struct MunRuntimeHandle handle, MunGcPtr obj);
 
 /**
- * Deallocates a string that was allocated by the runtime.
+ * Collects all memory that is no longer referenced by rooted objects. If successful, `reclaimed`
+ * is set, otherwise a non-zero error handle is returned. If `reclaimed` is `true`, memory was
+ * reclaimed, otherwise nothing happend. This behavior will likely change in the future.
+ *
+ * If a non-zero error handle is returned, it must be manually destructed using
+ * [`mun_error_destroy`].
  *
  * # Safety
  *
- * This function receives a raw pointer as parameter. Only when the argument is not a null pointer,
- * its content will be deallocated. Passing pointers to invalid data or memory allocated by other
- * processes, will lead to undefined behavior.
+ * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
+ * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-void mun_destroy_string(const char *string);
+struct MunErrorHandle mun_gc_collect(struct MunRuntimeHandle handle, bool *reclaimed);
 
 /**
- * Destructs the error corresponding to `error_handle`.
+ * Constructs a new runtime that loads the library at `library_path` and its dependencies. If
+ * successful, the runtime `handle` is set, otherwise a non-zero error handle is returned.
+ *
+ * If a non-zero error handle is returned, it must be manually destructed using
+ * [`mun_error_destroy`].
+ *
+ * The runtime must be manually destructed using [`mun_runtime_destroy`].
+ *
+ * # Safety
+ *
+ * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
+ * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-void mun_error_destroy(struct MunErrorHandle error_handle);
+struct MunErrorHandle mun_runtime_create(const char *library_path,
+                                         struct MunRuntimeOptions options,
+                                         struct MunRuntimeHandle *handle);
 
 /**
- * Retrieves the error message corresponding to `error_handle`. If the `error_handle` exists, a
- * valid `char` pointer is returned, otherwise a null-pointer is returned.
+ * Destructs the runtime corresponding to `handle`.
  */
-const char *mun_error_message(struct MunErrorHandle error_handle);
+void mun_runtime_destroy(struct MunRuntimeHandle handle);
 
 /**
- * Allocates an object in the runtime of the given `type_info`. If successful, `obj` is set,
- * otherwise a non-zero error handle is returned.
+ * Retrieves the [`FunctionDefinition`] for `fn_name` from the runtime corresponding to `handle`.
+ * If successful, `has_fn_info` and `fn_info` are set, otherwise a non-zero error handle is
+ * returned.
  *
  * If a non-zero error handle is returned, it must be manually destructed using
  * [`mun_error_destroy`].
@@ -331,13 +404,15 @@ const char *mun_error_message(struct MunErrorHandle error_handle);
  * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
  * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-struct MunErrorHandle mun_gc_alloc(struct MunRuntimeHandle handle,
-                                   MunUnsafeTypeInfo type_info,
-                                   MunGcPtr *obj);
+struct MunErrorHandle mun_runtime_get_function_info(struct MunRuntimeHandle handle,
+                                                    const char *fn_name,
+                                                    bool *has_fn_info,
+                                                    struct MunFunctionInfoHandle *fn_info);
 
 /**
- * Retrieves the `type_info` for the specified `obj` from the runtime. If successful, `type_info`
- * is set, otherwise a non-zero error handle is returned.
+ * Retrieves the type information corresponding to the specified `type_name` from the runtime.
+ * If successful, `has_type_info` and `type_info` are set, otherwise a non-zero error handle is
+ * returned.
  *
  * If a non-zero error handle is returned, it must be manually destructed using
  * [`mun_error_destroy`].
@@ -347,16 +422,15 @@ struct MunErrorHandle mun_gc_alloc(struct MunRuntimeHandle handle,
  * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
  * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-struct MunErrorHandle mun_gc_ptr_type(struct MunRuntimeHandle handle,
-                                      MunGcPtr obj,
-                                      MunUnsafeTypeInfo *type_info);
+struct MunErrorHandle mun_runtime_get_type_info_by_name(struct MunRuntimeHandle runtime,
+                                                        const char *type_name,
+                                                        bool *has_type_info,
+                                                        struct MunTypeInfoHandle *type_info);
 
 /**
- * Roots the specified `obj`, which keeps it and objects it references alive. Objects marked as
- * root, must call `mun_gc_unroot` before they can be collected. An object can be rooted multiple
- * times, but you must make sure to call `mun_gc_unroot` an equal number of times before the
- * object can be collected. If successful, `obj` has been rooted, otherwise a non-zero error handle
- * is returned.
+ * Retrieves the type information corresponding to the specified `type_id` from the runtime. If
+ * successful, `has_type_info` and `type_info` are set, otherwise a non-zero error handle is
+ * returned.
  *
  * If a non-zero error handle is returned, it must be manually destructed using
  * [`mun_error_destroy`].
@@ -366,13 +440,14 @@ struct MunErrorHandle mun_gc_ptr_type(struct MunRuntimeHandle handle,
  * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
  * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-struct MunErrorHandle mun_gc_root(struct MunRuntimeHandle handle, MunGcPtr obj);
+struct MunErrorHandle mun_runtime_get_type_info_by_id(struct MunRuntimeHandle runtime,
+                                                      const struct MunTypeId *type_id,
+                                                      bool *has_type_info,
+                                                      struct MunTypeInfoHandle *type_info);
 
 /**
- * Unroots the specified `obj`, potentially allowing it and objects it references to be
- * collected. An object can be rooted multiple times, so you must make sure to call `mun_gc_unroot`
- * the same number of times as `mun_gc_root` was called before the object can be collected. If
- * successful, `obj` has been unrooted, otherwise a non-zero error handle is returned.
+ * Updates the runtime corresponding to `handle`. If successful, `updated` is set, otherwise a
+ * non-zero error handle is returned.
  *
  * If a non-zero error handle is returned, it must be manually destructed using
  * [`mun_error_destroy`].
@@ -382,22 +457,231 @@ struct MunErrorHandle mun_gc_root(struct MunRuntimeHandle handle, MunGcPtr obj);
  * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
  * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
  */
-struct MunErrorHandle mun_gc_unroot(struct MunRuntimeHandle handle, MunGcPtr obj);
+struct MunErrorHandle mun_runtime_update(struct MunRuntimeHandle handle, bool *updated);
 
 /**
- * Collects all memory that is no longer referenced by rooted objects. If successful, `reclaimed`
- * is set, otherwise a non-zero error handle is returned. If `reclaimed` is `true`, memory was
- * reclaimed, otherwise nothing happend. This behavior will likely change in the future.
+ * Retrieves the field's name.
  *
- * If a non-zero error handle is returned, it must be manually destructed using
- * [`mun_error_destroy`].
+ * # Safety
+ *
+ * The caller is responsible for calling `mun_string_destroy` on the return pointer - if it is not null.
+ *
+ * This function might result in undefined behavior if the [`crate::TypeInfoHandle`] associated
+ * with this `FieldInfoHandle` has been deallocated.
+ */
+const char *mun_field_info_name(struct MunFieldInfoHandle field_info);
+
+/**
+ * Retrieves the field's type.
  *
  * # Safety
  *
- * This function receives raw pointers as parameters. If any of the arguments is a null pointer,
- * an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
+ * This method is considered unsafe because the passed `field_info` might have been deallocated by
+ * a call to [`mun_type_info_decrement_strong_count`] of the type that contains this field.
  */
-struct MunErrorHandle mun_gc_collect(struct MunRuntimeHandle handle, bool *reclaimed);
+struct MunTypeInfoHandle mun_field_info_type(struct MunFieldInfoHandle field_info);
+
+/**
+ * Retrieves the field's offset.
+ *
+ * # Safety
+ *
+ * This method is considered unsafe because the passed `field_info` might have been deallocated by
+ * a call to [`mun_type_info_decrement_strong_count`] of the type that contains this field.
+ */
+struct MunErrorHandle mun_field_info_offset(struct MunFieldInfoHandle field_info,
+                                            uint16_t *field_offset);
+
+/**
+ * Deallocates a span of `FieldInfo`s that was allocated by the runtime.
+ *
+ * Deallocating span only deallocates the data allocated for the span. Deallocating a span will not
+ * deallocate the FieldInfo's it references. `FieldInfo`s are destroyed when the top-level
+ * `TypeInfo` is destroyed.
+ *
+ * # Safety
+ *
+ * This function receives a span as parameter. Only when the spans data pointer is not null, is the
+ * content deallocated. Passing pointers to invalid data of memory allocated by other processes,
+ * will lead to undefined behavior.
+ */
+bool mun_field_info_span_destroy(struct MunFieldInfoSpan span);
+
+/**
+ * Decrements the strong count of the `Arc<FunctionDefinition>` associated with `handle`.
+ *
+ * # Safety
+ *
+ * This function might be unsafe if the underlying data has already been deallocated by a previous
+ * call to [`mun_function_info_decrement_strong_count`].
+ */
+bool mun_function_info_decrement_strong_count(struct MunFunctionInfoHandle fn_info);
+
+/**
+ * Increments the strong count of the `Arc<FunctionDefinition>` associated with `handle`.
+ *
+ * # Safety
+ *
+ * This function might be unsafe if the underlying data has been deallocated by a previous call
+ * to [`mun_function_info_decrement_strong_count`].
+ */
+bool mun_function_info_increment_strong_count(struct MunFunctionInfoHandle fn_info);
+
+/**
+ * Retrieves the function's function pointer.
+ *
+ * # Safety
+ *
+ * This function might be unsafe if the underlying data has been deallocated by a previous call
+ * to [`mun_function_info_decrement_strong_count`].
+ */
+const void *mun_function_info_fn_ptr(struct MunFunctionInfoHandle fn_info);
+
+/**
+ * Retrieves the function's name.
+ *
+ * # Safety
+ *
+ * The caller is responsible for calling `mun_string_destroy` on the return pointer - if it is not null.
+ *
+ * This function might be unsafe if the underlying data has been deallocated by a previous call
+ * to [`mun_function_info_decrement_strong_count`].
+ */
+const char *mun_function_info_name(struct MunFunctionInfoHandle fn_info);
+
+/**
+ * Retrieves the function's argument types.
+ *
+ * # Safety
+ *
+ * If a non-null handle is returned, the caller is responsible for calling
+ * `mun_type_info_span_destroy` on the returned handle.
+ *
+ * This function might be unsafe if the underlying data has been deallocated by a previous call
+ * to [`mun_function_info_decrement_strong_count`].
+ */
+struct MunErrorHandle mun_function_info_argument_types(struct MunFunctionInfoHandle fn_info,
+                                                       struct MunTypeInfoSpan *arg_types);
+
+/**
+ * Retrieves the function's return type.
+ *
+ * # Safety
+ *
+ * This function might be unsafe if the underlying data has been deallocated by a previous call
+ * to [`mun_function_info_decrement_strong_count`].
+ */
+struct MunTypeInfoHandle mun_function_info_return_type(struct MunFunctionInfoHandle fn_info);
+
+/**
+ * Retrieves information about the struct's fields.
+ *
+ * # Safety
+ *
+ * The caller is responsible for calling `mun_field_info_span_destroy` on the returned span.
+ *
+ * This function might result in undefined behavior if the [`crate::TypeInfoHandle`] associated
+ * with this `StructInfoHandle` has been deallocated.
+ */
+struct MunErrorHandle mun_struct_info_fields(struct MunStructInfoHandle struct_info,
+                                             struct MunFieldInfoSpan *field_info_span);
+
+/**
+ * Retrieves the struct's memory kind.
+ *
+ * # Safety
+ *
+ * This function might result in undefined behavior if the [`crate::TypeInfoHandle`] associated
+ * with this `StructInfoHandle` has been deallocated.
+ */
+struct MunErrorHandle mun_struct_info_memory_kind(struct MunStructInfoHandle struct_info,
+                                                  MunStructMemoryKind *memory_kind);
+
+/**
+ * Decrements the strong count of the `Arc<TypeInfo>` associated with `handle`.
+ *
+ * # Safety
+ *
+ * This function results in undefined behavior if the passed in `TypeInfoHandle` has already been
+ * deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+ */
+bool mun_type_info_decrement_strong_count(struct MunTypeInfoHandle handle);
+
+/**
+ * Increments the strong count of the `Arc<TypeInfo>` associated with `handle`.
+ *
+ * # Safety
+ *
+ * This function results in undefined behavior if the passed in `TypeInfoHandle` has already been
+ * deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+ */
+bool mun_type_info_increment_strong_count(struct MunTypeInfoHandle handle);
+
+/**
+ * Retrieves the type's ID.
+ *
+ * # Safety
+ *
+ * This function results in undefined behavior if the passed in `TypeInfoHandle` has been
+ * deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+ */
+struct MunErrorHandle mun_type_info_id(struct MunTypeInfoHandle type_info,
+                                       struct MunTypeId *type_id);
+
+/**
+ * Retrieves the type's name.
+ *
+ * # Safety
+ *
+ * The caller is responsible for calling `mun_string_destroy` on the return pointer - if it is not null.
+ *
+ * This function results in undefined behavior if the passed in `TypeInfoHandle` has been
+ * deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+ */
+const char *mun_type_info_name(struct MunTypeInfoHandle type_info);
+
+/**
+ * Retrieves the type's size.
+ *
+ * # Safety
+ *
+ * This function results in undefined behavior if the passed in `TypeInfoHandle` has been
+ * deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+ */
+struct MunErrorHandle mun_type_info_size(struct MunTypeInfoHandle type_info, uintptr_t *size);
+
+/**
+ * Retrieves the type's alignment.
+ *
+ * # Safety
+ *
+ * This function results in undefined behavior if the passed in `TypeInfoHandle` has been
+ * deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+ */
+struct MunErrorHandle mun_type_info_align(struct MunTypeInfoHandle type_info, uintptr_t *align);
+
+/**
+ * Retrieves the type's data.
+ *
+ * # Safety
+ *
+ * The original `TypeInfoHandle` needs to stay alive as long as the `TypeInfoData` lives. The
+ * `TypeInfoData` is destroyed at the same time as the `TypeInfo`. A `TypeInfo` might be destroyed
+ * through a call to [`mun_type_info_decrement_strong_count`].
+ */
+struct MunErrorHandle mun_type_info_data(struct MunTypeInfoHandle type_info,
+                                         union MunTypeInfoData *type_info_data);
+
+/**
+ * Deallocates an span of `TypeInfo` that was allocated by the runtime.
+ *
+ * # Safety
+ *
+ * This function receives a span as parameter. Only when the spans data pointer is not null, its
+ * content will be deallocated. Passing pointers to invalid data or memory allocated by other
+ * processes, will lead to undefined behavior.
+ */
+bool mun_type_info_span_destroy(struct MunTypeInfoSpan array_handle);
 
 #ifdef __cplusplus
 } // extern "C"
diff --git a/cpp/include/mun/function.h b/cpp/include/mun/runtime_function.h
similarity index 83%
rename from cpp/include/mun/function.h
rename to cpp/include/mun/runtime_function.h
index 3b2761642..2e6f59297 100644
--- a/cpp/include/mun/function.h
+++ b/cpp/include/mun/runtime_function.h
@@ -5,6 +5,7 @@
 #include <vector>
 
 #include "mun/runtime_capi.h"
+#include "mun/static_type_info.h"
 #include "mun/type_info.h"
 #include "mun/util.h"
 
@@ -21,8 +22,8 @@ struct RuntimeFunction {
     template <typename TRet, typename... TArgs>
     RuntimeFunction(std::string_view name, TRet(MUN_CALLTYPE* fn_ptr)(TArgs...))
         : name(name),
-          arg_types({arg_type_info<TArgs>()...}),
-          ret_type(return_type_info<TRet>()),
+          arg_types({StaticTypeInfo<TArgs>::id()...}),
+          ret_type(StaticTypeInfo<TRet>::id()),
           fn_ptr(reinterpret_cast<const void*>(fn_ptr)) {}
 
     RuntimeFunction(const RuntimeFunction&) = default;
@@ -31,8 +32,8 @@ struct RuntimeFunction {
     RuntimeFunction& operator=(RuntimeFunction&&) = default;
 
     std::string name;
-    std::vector<MunTypeInfo const*> arg_types;
-    std::optional<MunTypeInfo const*> ret_type;
+    std::vector<MunTypeId> arg_types;
+    MunTypeId ret_type;
     const void* fn_ptr;
 };
 }  // namespace mun
diff --git a/cpp/include/mun/static_type_info.h b/cpp/include/mun/static_type_info.h
new file mode 100644
index 000000000..b159fda35
--- /dev/null
+++ b/cpp/include/mun/static_type_info.h
@@ -0,0 +1,71 @@
+#ifndef MUN_STATIC_TYPE_INFO_H_
+#define MUN_STATIC_TYPE_INFO_H_
+
+#include "mun/runtime_capi.h"
+
+namespace mun {
+namespace details {
+constexpr MunTypeId type_id(const char* type_name) noexcept {
+    const auto hash = md5::compute(type_name);
+    return MunTypeId{MunGuid{
+        hash[0],
+        hash[1],
+        hash[2],
+        hash[3],
+        hash[4],
+        hash[5],
+        hash[6],
+        hash[7],
+        hash[8],
+        hash[9],
+        hash[10],
+        hash[11],
+        hash[12],
+        hash[13],
+        hash[14],
+        hash[15],
+    }};
+}
+}  // namespace details
+
+template <typename T>
+struct StaticTypeInfo;
+
+#define IMPL_PRIMITIVE_TYPE_INFO(ty, name_literal)                                            \
+    template <>                                                                               \
+    struct StaticTypeInfo<ty> {                                                               \
+        static constexpr MunTypeId id() noexcept {                                            \
+            return details::type_id(StaticTypeInfo<ty>::name());                              \
+        }                                                                                     \
+        static constexpr const char* name() noexcept { return name_literal; }                 \
+        static constexpr size_t size() noexcept { return sizeof(ty); }                        \
+        static constexpr size_t alignment() noexcept { return std::alignment_of<ty>::value; } \
+    };
+
+IMPL_PRIMITIVE_TYPE_INFO(bool, "core::bool");
+IMPL_PRIMITIVE_TYPE_INFO(float, "core::f32");
+IMPL_PRIMITIVE_TYPE_INFO(double, "core::f64");
+IMPL_PRIMITIVE_TYPE_INFO(int8_t, "core::i8");
+IMPL_PRIMITIVE_TYPE_INFO(int16_t, "core::i16");
+IMPL_PRIMITIVE_TYPE_INFO(int32_t, "core::i32");
+IMPL_PRIMITIVE_TYPE_INFO(int64_t, "core::i64");
+// IMPL_PRIMITIVE_TYPE_REFLECTION(int128_t, "core::i128");
+IMPL_PRIMITIVE_TYPE_INFO(uint8_t, "core::u8");
+IMPL_PRIMITIVE_TYPE_INFO(uint16_t, "core::u16");
+IMPL_PRIMITIVE_TYPE_INFO(uint32_t, "core::u32");
+IMPL_PRIMITIVE_TYPE_INFO(uint64_t, "core::u64");
+// IMPL_PRIMITIVE_TYPE_REFLECTION(uint128_t, "core::u128");
+
+template <>
+struct StaticTypeInfo<void> {
+    static constexpr MunTypeId id() noexcept {
+        return details::type_id(StaticTypeInfo<void>::name());
+    }
+    static constexpr const char* name() noexcept { return "core::()"; }
+    static constexpr size_t size() noexcept { return 0; }
+    static constexpr size_t alignment() noexcept { return 1; }
+};
+
+}  // namespace mun
+
+#endif  // MUN_STATIC_TYPE_INFO_H_
diff --git a/cpp/include/mun/struct_info.h b/cpp/include/mun/struct_info.h
new file mode 100644
index 000000000..e9f6398a8
--- /dev/null
+++ b/cpp/include/mun/struct_info.h
@@ -0,0 +1,47 @@
+#ifndef MUN_STRUCT_INFO_H
+#define MUN_STRUCT_INFO_H
+
+#include <cassert>
+
+#include "mun/field_info.h"
+#include "mun/runtime_capi.h"
+
+namespace mun {
+/**
+ * @brief A wrapper around a Mun struct information handle.
+ */
+class StructInfo {
+public:
+    /**
+     * @brief Constructs struct information from an instantiated `MunStructInfoHandle`.
+     */
+    StructInfo(MunStructInfoHandle handle) noexcept : m_handle(handle) {}
+
+    /**
+     * @brief Returns the struct's fields.
+     */
+    FieldInfoSpan fields() const noexcept {
+        MunFieldInfoSpan span;
+        const auto error_handle = mun_struct_info_fields(m_handle, &span);
+        assert(error_handle._0 == nullptr);
+
+        return FieldInfoSpan(span);
+    }
+
+    /**
+     * @brief Returns the struct's memory kind.
+     */
+    MunStructMemoryKind memory_kind() const noexcept {
+        MunStructMemoryKind memory_kind;
+        const auto error_handle = mun_struct_info_memory_kind(m_handle, &memory_kind);
+        assert(error_handle._0 == nullptr);
+
+        return memory_kind;
+    }
+
+private:
+    MunStructInfoHandle m_handle;
+};
+}  // namespace mun
+
+#endif  // MUN_STRUCT_INFO_H
diff --git a/cpp/include/mun/struct_ref.h b/cpp/include/mun/struct_ref.h
index 1bad27700..2d2ef14cb 100644
--- a/cpp/include/mun/struct_ref.h
+++ b/cpp/include/mun/struct_ref.h
@@ -11,23 +11,29 @@
 #include "mun/gc.h"
 #include "mun/marshal.h"
 #include "mun/runtime.h"
+#include "mun/static_type_info.h"
+#include "mun/struct_info.h"
+#include "mun/type_info.h"
 
 namespace mun {
 namespace details {
-inline std::optional<size_t> find_index(std::string_view type_name,
-                                        const MunStructInfo& struct_info,
-                                        std::string_view field_name) noexcept {
-    const auto begin = struct_info.field_names;
-    const auto end = struct_info.field_names + struct_info.num_fields;
-
-    const auto it = std::find(begin, end, field_name);
-    if (it == end) {
+inline std::optional<FieldInfo> find_field(std::string_view type_name,
+                                           const StructInfo& struct_info,
+                                           std::string_view field_name) noexcept {
+    const auto fields = struct_info.fields();
+
+    const auto it = std::find_if(fields.begin(), fields.end(),
+                                 [field_name](const MunFieldInfoHandle& field_handle) {
+                                     const FieldInfo field_info(field_handle);
+                                     return field_info.name() == field_name;
+                                 });
+    if (it == fields.end()) {
         std::cerr << "StructRef `" << type_name << "` does not contain field `" << field_name
                   << "`." << std::endl;
         return std::nullopt;
     }
 
-    return std::make_optional(std::distance(begin, it));
+    return std::make_optional(FieldInfo(*it));
 }
 
 inline std::string format_struct_field(std::string_view struct_name,
@@ -39,17 +45,13 @@ inline std::string format_struct_field(std::string_view struct_name,
 }
 }  // namespace details
 
-inline size_t type_info_size_in_bytes(const MunTypeInfo& type_info) noexcept {
-    return static_cast<size_t>((type_info.size_in_bits + 7) / 8);
-}
-
 /** Type-agnostic wrapper for interoperability with a Mun struct.
  *
  * Roots and unroots the underlying object upon construction and destruction,
  * respectively.
  */
 class StructRef {
-   public:
+public:
     /** Constructs a `StructRef` that wraps a raw Mun struct.
      *
      * \param runtime a reference to the runtime in which the object instance
@@ -58,7 +60,7 @@ class StructRef {
      */
     StructRef(const Runtime& runtime, MunGcPtr raw) noexcept
         : m_runtime(&runtime), m_handle(GcRootPtr(runtime, raw)) {
-        assert(runtime.ptr_type(raw)->data.tag == MunTypeInfoData_Tag::Struct);
+        assert(runtime.ptr_type(raw).data().tag == MunTypeInfoData_Tag::Struct);
     }
 
     StructRef(const StructRef&) noexcept = default;
@@ -79,7 +81,7 @@ class StructRef {
      *
      * \return a pointer to the struct's type information
      */
-    const MunUnsafeTypeInfo info() const noexcept { return m_runtime->ptr_type(raw()); }
+    TypeInfo info() const noexcept { return m_runtime->ptr_type(raw()); }
 
     /** Tries to retrieve the copied value of the field corresponding to
      * `field_name`.
@@ -109,7 +111,7 @@ class StructRef {
     template <typename T>
     bool set(std::string_view field_name, T value) noexcept;
 
-   private:
+private:
     const Runtime* m_runtime;
     GcRootPtr m_handle;
 };
@@ -125,19 +127,18 @@ struct Marshal<StructRef> {
     static type to(StructRef value) noexcept { return value.raw(); }
 
     static StructRef copy_from(const type* ptr, const Runtime& runtime,
-                               std::optional<const MunTypeInfo*> type_info) noexcept {
+                               const TypeInfo& type_info) noexcept {
         // Safety: `type_info_as_struct` is guaranteed to return a value for
         // `StructRef`s.
-        const auto& struct_info = type_info.value()->data.struct_;
+        const StructInfo struct_info(type_info.data().struct_);
 
         MunGcPtr gc_handle;
-        if (struct_info.memory_kind == MunStructMemoryKind::Value) {
+        if (struct_info.memory_kind() == MunStructMemoryKind::Value) {
             // Create a new managed object
-            gc_handle = *runtime.gc_alloc(const_cast<MunUnsafeTypeInfo>(type_info.value()));
+            gc_handle = *runtime.gc_alloc(type_info);
 
             // Copy the old object into the new object
-            const auto size = type_info_size_in_bytes(*type_info.value());
-            std::memcpy(*gc_handle, ptr, size);
+            std::memcpy(*gc_handle, ptr, type_info.size());
         } else {
             // For a gc struct, `ptr` points to a `MunGcPtr`.
             gc_handle = *ptr;
@@ -146,32 +147,31 @@ struct Marshal<StructRef> {
         return StructRef(runtime, gc_handle);
     }
 
-    static void move_to(type value, type* ptr,
-                        std::optional<const MunTypeInfo*> type_info) noexcept {
+    static void move_to(type value, type* ptr, const TypeInfo& type_info) noexcept {
         // Safety: `type_info_as_struct` is guaranteed to return a value for
         // `StructRef`s.
-        const auto& struct_info = type_info.value()->data.struct_;
-        if (struct_info.memory_kind == MunStructMemoryKind::Value) {
-            const auto size = type_info_size_in_bytes(*type_info.value());
+        const StructInfo struct_info(type_info.data().struct_);
+        if (struct_info.memory_kind() == MunStructMemoryKind::Value) {
             // Copy the `struct(value)` into the old object
-            std::memcpy(ptr, *value, size);
+            std::memcpy(ptr, *value, type_info.size());
         } else {
             *ptr = std::move(value);
         }
     }
 
     static StructRef swap_at(type value, type* ptr, const Runtime& runtime,
-                             std::optional<const MunTypeInfo*> type_info) noexcept {
+                             const TypeInfo& type_info) noexcept {
         // Safety: `type_info_as_struct` is guaranteed to return a value for
         // `StructRef`s.
-        const auto& struct_info = type_info.value()->data.struct_;
+        const StructInfo struct_info(type_info.data().struct_);
 
         MunGcPtr gc_handle;
-        if (struct_info.memory_kind == MunStructMemoryKind::Value) {
+        if (struct_info.memory_kind() == MunStructMemoryKind::Value) {
             // Create a new managed object
-            gc_handle = *runtime.gc_alloc(const_cast<MunUnsafeTypeInfo>(type_info.value()));
+            gc_handle = *runtime.gc_alloc(type_info);
+
+            const auto size = type_info.size();
 
-            const auto size = type_info_size_in_bytes(*type_info.value());
             // Copy the old object into the new object
             std::memcpy(*gc_handle, ptr, size);
             // Copy the `struct(value)` into the old object
@@ -192,14 +192,14 @@ namespace mun {
 
 template <>
 struct ArgumentReflection<StructRef> {
-    static const char* type_name(const StructRef& s) noexcept { return s.info()->name; }
-    static MunGuid type_guid(const StructRef& s) noexcept { return s.info()->guid; }
+    static const char* type_name(const StructRef& s) noexcept { return s.info().name().data(); }
+    static MunTypeId type_id(const StructRef& s) noexcept { return s.info().id(); }
 };
 
 template <>
 struct ReturnTypeReflection<StructRef> {
     static constexpr const char* type_name() noexcept { return "struct"; }
-    static constexpr MunGuid type_guid() noexcept { return details::type_guid(type_name()); }
+    static constexpr MunTypeId type_id() noexcept { return details::type_id(type_name()); }
 };
 
 template <typename T>
@@ -208,25 +208,24 @@ std::optional<T> StructRef::get(std::string_view field_name) const noexcept {
 
     // Safety: `type_info_as_struct` is guaranteed to return a value for
     // `StructRef`s.
-    const auto& struct_info = type_info->data.struct_;
-    if (const auto idx = details::find_index(type_info->name, struct_info, field_name)) {
-        const auto* field_type = struct_info.field_types[*idx];
-        if (auto diff = reflection::equals_return_type<T>(*field_type)) {
+    const StructInfo struct_info(type_info.data().struct_);
+    if (const auto field_info = details::find_field(type_info.name(), struct_info, field_name)) {
+        if (auto diff = reflection::equals_return_type<T>(field_info->type())) {
             const auto& [expected, found] = *diff;
 
             std::cerr << "Mismatched types for `"
-                      << details::format_struct_field(type_info->name, field_name)
+                      << details::format_struct_field(type_info.name(), field_name)
                       << "`. Expected: `" << expected << "`. Found: `" << found << "`."
                       << std::endl;
 
             return std::nullopt;
         }
 
-        const auto offset = static_cast<size_t>(struct_info.field_offsets[*idx]);
+        const auto offset = static_cast<size_t>(field_info->offset());
         const auto byte_ptr = reinterpret_cast<const std::byte*>(*raw());
         return std::make_optional(Marshal<T>::copy_from(
             reinterpret_cast<const typename Marshal<T>::type*>(byte_ptr + offset), *m_runtime,
-            field_type ? std::make_optional(field_type) : std::nullopt));
+            field_info->type()));
     } else {
         return std::nullopt;
     }
@@ -237,26 +236,24 @@ std::optional<T> StructRef::replace(std::string_view field_name, T value) noexce
 
     // Safety: `type_info_as_struct` is guaranteed to return a value for
     // `StructRef`s.
-    const auto& struct_info = type_info->data.struct_;
-    if (const auto idx = details::find_index(type_info->name, struct_info, field_name)) {
-        const auto* field_type = struct_info.field_types[*idx];
-        if (auto diff = reflection::equals_return_type<T>(*field_type)) {
+    const StructInfo struct_info(type_info.data().struct_);
+    if (const auto field_info = details::find_field(type_info.name(), struct_info, field_name)) {
+        if (auto diff = reflection::equals_return_type<T>(field_info->type())) {
             const auto& [expected, found] = *diff;
 
             std::cerr << "Mismatched types for `"
-                      << details::format_struct_field(type_info->name, field_name)
+                      << details::format_struct_field(type_info.name(), field_name)
                       << "`. Expected: `" << expected << "`. Found: `" << found << "`."
                       << std::endl;
 
             return std::nullopt;
         }
 
-        const auto offset = static_cast<size_t>(struct_info.field_offsets[*idx]);
         auto byte_ptr = reinterpret_cast<std::byte*>(*raw());
         return std::make_optional(Marshal<T>::swap_at(
             Marshal<T>::to(std::move(value)),
-            reinterpret_cast<typename Marshal<T>::type*>(byte_ptr + offset), *m_runtime,
-            field_type ? std::make_optional(field_type) : std::nullopt));
+            reinterpret_cast<typename Marshal<T>::type*>(byte_ptr + field_info->offset()),
+            *m_runtime, field_info->type()));
     } else {
         return std::nullopt;
     }
@@ -267,26 +264,25 @@ bool StructRef::set(std::string_view field_name, T value) noexcept {
 
     // Safety: `type_info_as_struct` is guaranteed to return a value for
     // `StructRef`s.
-    const auto& struct_info = type_info->data.struct_;
-    if (const auto idx = details::find_index(type_info->name, struct_info, field_name)) {
-        const auto* field_type = struct_info.field_types[*idx];
-        if (auto diff = reflection::equals_return_type<T>(*field_type)) {
+    const StructInfo struct_info(type_info.data().struct_);
+    if (const auto field_info = details::find_field(type_info.name(), struct_info, field_name)) {
+        if (auto diff = reflection::equals_return_type<T>(field_info->type())) {
             const auto& [expected, found] = *diff;
 
             std::cerr << "Mismatched types for `"
-                      << details::format_struct_field(type_info->name, field_name)
+                      << details::format_struct_field(type_info.name(), field_name)
                       << "`. Expected: `" << expected << "`. Found: `" << found << "`."
                       << std::endl;
 
             return false;
         }
 
-        const auto offset = static_cast<size_t>(struct_info.field_offsets[*idx]);
         auto byte_ptr = reinterpret_cast<std::byte*>(*raw());
 
-        Marshal<T>::move_to(Marshal<T>::to(std::move(value)),
-                            reinterpret_cast<typename Marshal<T>::type*>(byte_ptr + offset),
-                            field_type ? std::make_optional(field_type) : std::nullopt);
+        Marshal<T>::move_to(
+            Marshal<T>::to(std::move(value)),
+            reinterpret_cast<typename Marshal<T>::type*>(byte_ptr + field_info->offset()),
+            field_info->type());
         return true;
     } else {
         return false;
diff --git a/cpp/include/mun/type_info.h b/cpp/include/mun/type_info.h
index 3565f7150..c99420f9a 100644
--- a/cpp/include/mun/type_info.h
+++ b/cpp/include/mun/type_info.h
@@ -7,71 +7,143 @@
 #include "mun/runtime_capi.h"
 
 namespace mun {
-namespace details {
-constexpr MunGuid type_guid(const char* type_name) noexcept {
-    const auto hash = md5::compute(type_name);
-    return MunGuid{
-        hash[0], hash[1], hash[2],  hash[3],  hash[4],  hash[5],  hash[6],  hash[7],
-        hash[8], hash[9], hash[10], hash[11], hash[12], hash[13], hash[14], hash[15],
-    };
-}
-}  // namespace details
-
-template <typename T>
-struct TypeInfo;
-
-#define IMPL_PRIMITIVE_TYPE_INFO(ty, name_literal) \
-    template <>                                    \
-    struct TypeInfo<ty> {                          \
-        static constexpr MunTypeInfo Type{         \
-            details::type_guid(name_literal),      \
-            name_literal,                          \
-            sizeof(ty),                            \
-            std::alignment_of<ty>::value,          \
-            MunTypeInfoData_Tag::Primitive,        \
-        };                                         \
+/**
+ * @brief A wrapper around a Mun type information handle.
+ */
+class TypeInfo {
+public:
+    /**
+     * @brief Constructs type information from an instantiated `MunTypeInfoHandle`.
+     */
+    TypeInfo(MunTypeInfoHandle handle) noexcept : m_handle(handle) {}
+
+    ~TypeInfo() noexcept {
+        mun_type_info_decrement_strong_count(m_handle);
+        m_handle._0 = nullptr;
     }
 
-IMPL_PRIMITIVE_TYPE_INFO(bool, "core::bool");
-IMPL_PRIMITIVE_TYPE_INFO(float, "core::f32");
-IMPL_PRIMITIVE_TYPE_INFO(double, "core::f64");
-IMPL_PRIMITIVE_TYPE_INFO(int8_t, "core::i8");
-IMPL_PRIMITIVE_TYPE_INFO(int16_t, "core::i16");
-IMPL_PRIMITIVE_TYPE_INFO(int32_t, "core::i32");
-IMPL_PRIMITIVE_TYPE_INFO(int64_t, "core::i64");
-// IMPL_PRIMITIVE_TYPE_REFLECTION(int128_t, "core::i128");
-IMPL_PRIMITIVE_TYPE_INFO(uint8_t, "core::u8");
-IMPL_PRIMITIVE_TYPE_INFO(uint16_t, "core::u16");
-IMPL_PRIMITIVE_TYPE_INFO(uint32_t, "core::u32");
-IMPL_PRIMITIVE_TYPE_INFO(uint64_t, "core::u64");
-// IMPL_PRIMITIVE_TYPE_REFLECTION(uint128_t, "core::u128");
+    TypeInfo(const TypeInfo& other) noexcept : m_handle(other.m_handle) {
+        mun_type_info_increment_strong_count(m_handle);
+    }
 
-/**
- * Returns the return type `MunTypeInfo` corresponding to type T, or none if the return type is
- * void.
- */
-template <typename T>
-std::optional<MunTypeInfo const*> return_type_info() {
-    return &TypeInfo<T>::Type;
-}
+    TypeInfo(TypeInfo&& other) noexcept : m_handle(other.m_handle) { other.m_handle._0 = nullptr; }
 
-/**
- * Returns the return type `MunTypeInfo` corresponding to type T, or none if the return type is
- * void.
- */
-template <>
-inline std::optional<MunTypeInfo const*> return_type_info<void>() {
-    return std::nullopt;
-}
+    TypeInfo& operator=(const TypeInfo& other) {
+        m_handle = other.m_handle;
+        mun_type_info_increment_strong_count(m_handle);
+        return *this;
+    }
+
+    TypeInfo& operator=(TypeInfo&& other) {
+        m_handle = other.m_handle;
+        other.m_handle._0 = nullptr;
+        return *this;
+    }
+
+    /**
+     * @brief Retrieves the type's ID.
+     */
+    MunTypeId id() const noexcept {
+        MunTypeId id;
+
+        const auto error_handle = mun_type_info_id(m_handle, &id);
+        assert(error_handle._0 == nullptr);
+
+        return id;
+    }
+
+    /**
+     * @brief Retrieves the type's name.
+     */
+    std::string_view name() const noexcept {
+        const auto ptr = mun_type_info_name(m_handle);
+        assert(ptr);
+        return ptr;
+    }
+
+    /**
+     * @brief Retrieves the type's size in bytes.
+     */
+    size_t size() const noexcept {
+        size_t size;
+        const auto error_handle = mun_type_info_size(m_handle, &size);
+        assert(error_handle._0 == nullptr);
+        return size;
+    }
+
+    /**
+     * @brief Retrieves the type's alignment.
+     */
+    size_t alignment() const noexcept {
+        size_t align;
+        const auto error_handle = mun_type_info_align(m_handle, &align);
+        assert(error_handle._0 == nullptr);
+        return align;
+    }
+
+    /**
+     * @brief Retrieves the type's data.
+     *
+     * @details The returned data will only contain valid data as long as the type information
+     * object is in scope.
+     */
+    MunTypeInfoData data() const noexcept {
+        MunTypeInfoData data;
+        const auto error_handle = mun_type_info_data(m_handle, &data);
+        assert(error_handle._0 == nullptr);
+        return data;
+    }
+
+    /**
+     * @brief Returns the type's handle.
+     */
+    MunTypeInfoHandle handle() const noexcept { return m_handle; }
+
+private:
+    MunTypeInfoHandle m_handle;
+};
 
 /**
- * Returns the argument type `MunTypeInfo` corresponding to type T.
+ * @brief A wrapper around a span of Mun type informations, which are owned by the Mun runtime.
  */
-template <typename T>
-MunTypeInfo const* arg_type_info() {
-    return &TypeInfo<T>::Type;
-}
+class TypeInfoSpan {
+public:
+    /**
+     * @brief Constructs a type information span from an instantiated `MunTypeInfoSpan`.
+     */
+    TypeInfoSpan(MunTypeInfoSpan span) noexcept : m_span(span) {}
+
+    ~TypeInfoSpan() noexcept { mun_type_info_span_destroy(m_span); }
+
+    TypeInfoSpan(const TypeInfoSpan&) = delete;
+    TypeInfoSpan& operator=(const TypeInfoSpan&) = delete;
+
+    TypeInfoSpan(TypeInfoSpan&&) = default;
+    TypeInfoSpan& operator=(TypeInfoSpan&&) = default;
+
+    /**
+     * @brief Returns an iterator to the beginning.
+     */
+    const MunTypeInfoHandle* begin() const noexcept { return data(); }
+
+    /**
+     * @brief Returns an iterator to the end.
+     */
+    const MunTypeInfoHandle* end() const noexcept { return data() + size() + 1; }
+
+    /**
+     * @brief Returns a pointer to the beginning of the sequence of elements.
+     */
+    const MunTypeInfoHandle* data() const noexcept { return m_span.data; }
+
+    /**
+     * @brief Returns the number of elements in the sequence.
+     */
+    size_t size() const noexcept { return m_span.len; }
 
+private:
+    MunTypeInfoSpan m_span;
+};
 }  // namespace mun
 
 #endif  // MUN_TYPE_INFO_H
diff --git a/crates/mun/src/ops/start.rs b/crates/mun/src/ops/start.rs
index 93cda89a6..5b9dd7f52 100644
--- a/crates/mun/src/ops/start.rs
+++ b/crates/mun/src/ops/start.rs
@@ -1,5 +1,5 @@
 use anyhow::anyhow;
-use mun_runtime::{ReturnTypeReflection, Runtime};
+use mun_runtime::Runtime;
 use std::path::PathBuf;
 
 use crate::ExitStatus;
@@ -30,38 +30,36 @@ pub fn start(args: Args) -> anyhow::Result<ExitStatus> {
             )
         })?;
 
-    if let Some(ret_type) = fn_definition.prototype.signature.return_type() {
-        let type_guid = &ret_type.guid;
-        if *type_guid == bool::type_guid() {
-            let result: bool = runtime
-                .invoke(&args.entry, ())
-                .map_err(|e| anyhow!("{}", e))?;
+    let return_type = &fn_definition.prototype.signature.return_type;
+    if return_type.equals::<bool>() {
+        let result: bool = runtime
+            .invoke(&args.entry, ())
+            .map_err(|e| anyhow!("{}", e))?;
 
-            println!("{}", result)
-        } else if *type_guid == f64::type_guid() {
-            let result: f64 = runtime
-                .invoke(&args.entry, ())
-                .map_err(|e| anyhow!("{}", e))?;
+        println!("{}", result)
+    } else if return_type.equals::<f64>() {
+        let result: f64 = runtime
+            .invoke(&args.entry, ())
+            .map_err(|e| anyhow!("{}", e))?;
 
-            println!("{}", result)
-        } else if *type_guid == i64::type_guid() {
-            let result: i64 = runtime
-                .invoke(&args.entry, ())
-                .map_err(|e| anyhow!("{}", e))?;
+        println!("{}", result)
+    } else if return_type.equals::<i64>() {
+        let result: i64 = runtime
+            .invoke(&args.entry, ())
+            .map_err(|e| anyhow!("{}", e))?;
 
-            println!("{}", result)
-        } else {
-            return Err(anyhow!(
-                "Only native Mun return types are supported for entry points. Found: {}",
-                ret_type.name()
-            ));
-        };
-        Ok(ExitStatus::Success)
-    } else {
+        println!("{}", result)
+    } else if return_type.equals::<()>() {
         #[allow(clippy::unit_arg)]
         runtime
             .invoke(&args.entry, ())
             .map(|_: ()| ExitStatus::Success)
-            .map_err(|e| anyhow!("{}", e))
-    }
+            .map_err(|e| anyhow!("{}", e))?;
+    } else {
+        return Err(anyhow!(
+            "Only native Mun return types are supported for entry points. Found: {}",
+            return_type.name
+        ));
+    };
+    Ok(ExitStatus::Success)
 }
diff --git a/crates/mun_abi/Cargo.toml b/crates/mun_abi/Cargo.toml
index aaaa176bd..dc2fe7226 100644
--- a/crates/mun_abi/Cargo.toml
+++ b/crates/mun_abi/Cargo.toml
@@ -15,4 +15,6 @@ categories = ["api-bindings", "game-development", "mun"]
 [dependencies]
 md5 = "0.7.0"
 once_cell = "1.4.0"
+itertools = "0.10.3"
 parking_lot = "0.12.0"
+static_type_map = "0.5.2"
diff --git a/crates/mun_abi/src/assembly_info.rs b/crates/mun_abi/src/assembly_info.rs
index ee871094d..7b6c707ed 100644
--- a/crates/mun_abi/src/assembly_info.rs
+++ b/crates/mun_abi/src/assembly_info.rs
@@ -1,4 +1,4 @@
-use crate::{DispatchTable, ModuleInfo};
+use crate::{DispatchTable, ModuleInfo, TypeLut};
 use std::{ffi::CStr, os::raw::c_char, slice, str};
 
 /// Represents an assembly declaration.
@@ -6,8 +6,10 @@ use std::{ffi::CStr, os::raw::c_char, slice, str};
 pub struct AssemblyInfo {
     /// Symbols of the top-level module
     pub symbols: ModuleInfo,
-    /// Dispatch table
+    /// Function dispatch table
     pub dispatch_table: DispatchTable,
+    /// Type lookup table
+    pub type_lut: TypeLut,
     /// Paths to assembly dependencies
     pub(crate) dependencies: *const *const c_char,
     /// Number of dependencies
@@ -35,7 +37,7 @@ unsafe impl Sync for AssemblyInfo {}
 #[cfg(test)]
 mod tests {
     use crate::test_utils::{
-        fake_assembly_info, fake_dispatch_table, fake_module_info, FAKE_DEPENDENCY,
+        fake_assembly_info, fake_dispatch_table, fake_module_info, fake_type_lut, FAKE_DEPENDENCY,
         FAKE_MODULE_PATH,
     };
     use std::ffi::CString;
@@ -46,10 +48,11 @@ mod tests {
         let module = fake_module_info(&module_path, &[], &[]);
 
         let dispatch_table = fake_dispatch_table(&[], &mut []);
+        let type_lut = fake_type_lut(&[], &mut [], &[]);
 
         let dependency = CString::new(FAKE_DEPENDENCY).expect("Invalid fake dependency.");
         let dependencies = &[dependency.as_ptr()];
-        let assembly = fake_assembly_info(module, dispatch_table, dependencies);
+        let assembly = fake_assembly_info(module, dispatch_table, type_lut, dependencies);
 
         assert_eq!(assembly.dependencies().count(), dependencies.len());
         for (lhs, rhs) in assembly.dependencies().zip([FAKE_DEPENDENCY].iter()) {
diff --git a/crates/mun_abi/src/dispatch_table.rs b/crates/mun_abi/src/dispatch_table.rs
index 06cca44a3..2f6851d77 100644
--- a/crates/mun_abi/src/dispatch_table.rs
+++ b/crates/mun_abi/src/dispatch_table.rs
@@ -132,7 +132,7 @@ mod tests {
         let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
         let type_info = fake_type_info(&type_name, 1, 1, TypeInfoData::Primitive);
 
-        let return_type = Some(&type_info);
+        let return_type = Some(type_info.id);
         let fn_name = CString::new(FAKE_FN_NAME).expect("Invalid fake fn name.");
         let fn_prototype = fake_fn_prototype(&fn_name, &[], return_type);
 
@@ -147,7 +147,7 @@ mod tests {
             assert_eq!(lhs.0, rhs.0);
             assert_eq!(lhs.1.name(), rhs.1.name());
             assert_eq!(lhs.1.signature.arg_types(), rhs.1.signature.arg_types());
-            assert_eq!(lhs.1.signature.return_type(), rhs.1.signature.return_type());
+            assert_eq!(lhs.1.signature.return_type, rhs.1.signature.return_type);
         }
     }
 
@@ -165,7 +165,7 @@ mod tests {
         let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
         let type_info = fake_type_info(&type_name, 1, 1, TypeInfoData::Primitive);
 
-        let return_type = Some(&type_info);
+        let return_type = Some(type_info.id);
         let fn_name = CString::new(FAKE_FN_NAME).expect("Invalid fake fn name.");
         let fn_prototype = fake_fn_prototype(&fn_name, &[], return_type);
 
@@ -194,7 +194,7 @@ mod tests {
         let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
         let type_info = fake_type_info(&type_name, 1, 1, TypeInfoData::Primitive);
 
-        let return_type = Some(&type_info);
+        let return_type = Some(type_info.id);
         let fn_name = CString::new(FAKE_FN_NAME).expect("Invalid fake fn name.");
         let fn_prototype = fake_fn_prototype(&fn_name, &[], return_type);
 
@@ -207,7 +207,7 @@ mod tests {
         for (lhs, rhs) in result.iter().zip(prototypes.iter()) {
             assert_eq!(lhs.name(), rhs.name());
             assert_eq!(lhs.signature.arg_types(), rhs.signature.arg_types());
-            assert_eq!(lhs.signature.return_type(), rhs.signature.return_type());
+            assert_eq!(lhs.signature.return_type, rhs.signature.return_type);
         }
     }
 
@@ -216,7 +216,7 @@ mod tests {
         let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
         let type_info = fake_type_info(&type_name, 1, 1, TypeInfoData::Primitive);
 
-        let return_type = Some(&type_info);
+        let return_type = Some(type_info.id);
         let fn_name = CString::new(FAKE_FN_NAME).expect("Invalid fake fn name.");
         let fn_prototype = fake_fn_prototype(&fn_name, &[], return_type);
 
@@ -232,7 +232,7 @@ mod tests {
         let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
         let type_info = fake_type_info(&type_name, 1, 1, TypeInfoData::Primitive);
 
-        let return_type = Some(&type_info);
+        let return_type = Some(type_info.id);
         let fn_name = CString::new(FAKE_FN_NAME).expect("Invalid fake fn name.");
         let fn_prototype = fake_fn_prototype(&fn_name, &[], return_type);
 
@@ -248,7 +248,7 @@ mod tests {
         let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
         let type_info = fake_type_info(&type_name, 1, 1, TypeInfoData::Primitive);
 
-        let return_type = Some(&type_info);
+        let return_type = Some(type_info.id);
         let fn_name = CString::new(FAKE_FN_NAME).expect("Invalid fake fn name.");
         let fn_prototype = fake_fn_prototype(&fn_name, &[], return_type);
 
@@ -264,7 +264,7 @@ mod tests {
         let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
         let type_info = fake_type_info(&type_name, 1, 1, TypeInfoData::Primitive);
 
-        let return_type = Some(&type_info);
+        let return_type = Some(type_info.id);
         let fn_name = CString::new(FAKE_FN_NAME).expect("Invalid fake fn name.");
         let fn_prototype = fake_fn_prototype(&fn_name, &[], return_type);
 
@@ -283,7 +283,7 @@ mod tests {
         let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
         let type_info = fake_type_info(&type_name, 1, 1, TypeInfoData::Primitive);
 
-        let return_type = Some(&type_info);
+        let return_type = Some(type_info.id);
         let fn_name = CString::new(FAKE_FN_NAME).expect("Invalid fake fn name.");
         let fn_prototype = fake_fn_prototype(&fn_name, &[], return_type);
 
@@ -299,7 +299,7 @@ mod tests {
         let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
         let type_info = fake_type_info(&type_name, 1, 1, TypeInfoData::Primitive);
 
-        let return_type = Some(&type_info);
+        let return_type = Some(type_info.id);
         let fn_name = CString::new(FAKE_FN_NAME).expect("Invalid fake fn name.");
         let fn_prototype = fake_fn_prototype(&fn_name, &[], return_type);
 
diff --git a/crates/mun_abi/src/function_info.rs b/crates/mun_abi/src/function_info.rs
index 7ffa00642..809fea4d8 100644
--- a/crates/mun_abi/src/function_info.rs
+++ b/crates/mun_abi/src/function_info.rs
@@ -1,9 +1,8 @@
-use crate::{HasStaticTypeInfo, TypeInfo};
+use crate::{HasStaticTypeInfo, TypeId, TypeInfo};
 use std::{
     ffi::{c_void, CStr, CString},
-    fmt::{self, Formatter},
     os::raw::c_char,
-    ptr, slice, str,
+    slice, str,
 };
 
 /// Represents a function definition. A function definition contains the name, type signature, and
@@ -35,9 +34,9 @@ pub struct FunctionPrototype {
 #[derive(Clone)]
 pub struct FunctionSignature {
     /// Argument types
-    pub(crate) arg_types: *const *const TypeInfo,
+    pub arg_types: *const TypeId,
     /// Optional return type
-    pub(crate) return_type: *const TypeInfo,
+    pub return_type: TypeId,
     /// Number of argument types
     pub num_arg_types: u16,
 }
@@ -45,7 +44,7 @@ pub struct FunctionSignature {
 /// Owned storage for C-style `FunctionDefinition`.
 pub struct FunctionDefinitionStorage {
     _name: CString,
-    _type_infos: Vec<&'static TypeInfo>,
+    _arg_types: Vec<TypeId>,
 }
 
 unsafe impl Send for FunctionDefinition {}
@@ -58,67 +57,32 @@ impl FunctionPrototype {
     }
 }
 
-impl fmt::Display for FunctionPrototype {
-    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
-        write!(f, "fn {}(", self.name())?;
-        for (i, arg) in self.signature.arg_types().iter().enumerate() {
-            if i > 0 {
-                write!(f, ", ")?;
-            }
-            write!(f, "{}", arg)?;
-        }
-        write!(f, ")")?;
-        if let Some(ret_type) = self.signature.return_type() {
-            write!(f, ":{}", ret_type)?
-        }
-        Ok(())
-    }
-}
-
 unsafe impl Send for FunctionPrototype {}
 unsafe impl Sync for FunctionPrototype {}
 
 impl FunctionSignature {
     /// Returns the function's arguments' types.
-    pub fn arg_types(&self) -> &[&TypeInfo] {
+    pub fn arg_types(&self) -> &[TypeId] {
         if self.num_arg_types == 0 {
             &[]
         } else {
-            unsafe {
-                slice::from_raw_parts(
-                    self.arg_types.cast::<&TypeInfo>(),
-                    self.num_arg_types as usize,
-                )
-            }
+            unsafe { slice::from_raw_parts(self.arg_types, self.num_arg_types as usize) }
         }
     }
 
-    /// Returns the function's return type
-    pub fn return_type(&self) -> Option<&TypeInfo> {
-        unsafe { self.return_type.as_ref() }
-    }
-}
-
-impl fmt::Display for FunctionSignature {
-    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
-        write!(f, "fn(")?;
-        for (i, arg) in self.arg_types().iter().enumerate() {
-            if i > 0 {
-                write!(f, ", ")?;
-            }
-            write!(f, "{}", arg)?;
-        }
-        write!(f, ")")?;
-        if let Some(ret_type) = self.return_type() {
-            write!(f, ":{}", ret_type)?
+    /// Returns the function's return type.
+    pub fn return_type(&self) -> Option<TypeId> {
+        if self.return_type == <()>::type_info().id {
+            None
+        } else {
+            Some(self.return_type.clone())
         }
-        Ok(())
     }
 }
 
 impl PartialEq for FunctionSignature {
     fn eq(&self, other: &Self) -> bool {
-        self.return_type() == other.return_type()
+        self.return_type == other.return_type
             && self.arg_types().len() == other.arg_types().len()
             && self
                 .arg_types()
@@ -139,26 +103,19 @@ impl FunctionDefinitionStorage {
     pub fn new_function(
         name: &str,
         args: &[&'static TypeInfo],
-        ret: Option<&'static TypeInfo>,
+        ret: &'static TypeInfo,
         fn_ptr: *const c_void,
     ) -> (FunctionDefinition, FunctionDefinitionStorage) {
         let name = CString::new(name).unwrap();
-        let type_infos = args.to_vec();
-
-        let num_arg_types = type_infos.len() as u16;
-        let return_type = if let Some(ty) = ret {
-            ty as *const _
-        } else {
-            ptr::null()
-        };
+        let arg_types: Vec<TypeId> = args.iter().map(|ty| ty.id.clone()).collect();
 
         let fn_info = FunctionDefinition {
             prototype: FunctionPrototype {
                 name: name.as_ptr(),
                 signature: FunctionSignature {
-                    arg_types: type_infos.as_ptr() as *const *const _,
-                    return_type,
-                    num_arg_types,
+                    arg_types: arg_types.as_ptr(),
+                    return_type: ret.id.clone(),
+                    num_arg_types: arg_types.len() as u16,
                 },
             },
             fn_ptr,
@@ -166,7 +123,7 @@ impl FunctionDefinitionStorage {
 
         let fn_storage = FunctionDefinitionStorage {
             _name: name,
-            _type_infos: type_infos,
+            _arg_types: arg_types,
         };
 
         (fn_info, fn_storage)
@@ -191,20 +148,7 @@ macro_rules! into_function_info_impl {
                     FunctionDefinitionStorage::new_function(
                         name.as_ref(),
                         &[$($T::type_info(),)*],
-                        Some($R::type_info()),
-                        self as *const std::ffi::c_void,
-                    )
-                }
-            }
-
-            impl<$($T: HasStaticTypeInfo,)*> IntoFunctionDefinition
-            for extern "C" fn($($T),*)
-            {
-                fn into<S: AsRef<str>>(self, name: S) -> (FunctionDefinition, FunctionDefinitionStorage) {
-                    FunctionDefinitionStorage::new_function(
-                        name.as_ref(),
-                        &[$($T::type_info(),)*],
-                        None,
+                        $R::type_info(),
                         self as *const std::ffi::c_void,
                     )
                 }
@@ -258,7 +202,7 @@ mod tests {
         let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
         let type_info = fake_type_info(&type_name, 1, 1, TypeInfoData::Primitive);
 
-        let arg_types = &[&type_info];
+        let arg_types = &[type_info.id];
         let fn_signature = fake_fn_signature(arg_types, None);
 
         assert_eq!(fn_signature.arg_types(), arg_types);
@@ -267,7 +211,7 @@ mod tests {
     #[test]
     fn test_fn_signature_return_type_none() {
         let return_type = None;
-        let fn_signature = fake_fn_signature(&[], return_type);
+        let fn_signature = fake_fn_signature(&[], return_type.clone());
 
         assert_eq!(fn_signature.return_type(), return_type);
     }
@@ -277,8 +221,8 @@ mod tests {
         let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
         let type_info = fake_type_info(&type_name, 1, 1, TypeInfoData::Primitive);
 
-        let return_type = Some(&type_info);
-        let fn_signature = fake_fn_signature(&[], return_type);
+        let return_type = Some(type_info.id);
+        let fn_signature = fake_fn_signature(&[], return_type.clone());
 
         assert_eq!(fn_signature.return_type(), return_type);
     }
diff --git a/crates/mun_abi/src/lib.rs b/crates/mun_abi/src/lib.rs
index 3b6e7414f..3cd1406fc 100644
--- a/crates/mun_abi/src/lib.rs
+++ b/crates/mun_abi/src/lib.rs
@@ -9,13 +9,16 @@ mod assembly_info;
 mod dispatch_table;
 mod function_info;
 mod module_info;
-mod static_type_map;
+pub mod static_type_map;
 mod struct_info;
 mod type_info;
+mod type_lut;
 
 #[cfg(test)]
 mod test_utils;
 
+use std::fmt;
+
 pub use assembly_info::AssemblyInfo;
 pub use dispatch_table::DispatchTable;
 pub use function_info::{
@@ -24,13 +27,16 @@ pub use function_info::{
 };
 pub use module_info::ModuleInfo;
 pub use struct_info::{StructInfo, StructMemoryKind};
-pub use type_info::{HasStaticTypeInfo, TypeInfo, TypeInfoData};
+pub use type_info::{HasStaticTypeInfo, HasStaticTypeInfoName, TypeInfo, TypeInfoData};
+pub use type_lut::{TypeId, TypeLut};
 
 /// The Mun ABI prelude
 ///
 /// The *prelude* contains imports that are used almost every time.
 pub mod prelude {
-    pub use crate::{HasStaticTypeInfo, IntoFunctionDefinition, StructMemoryKind};
+    pub use crate::{
+        HasStaticTypeInfo, HasStaticTypeInfoName, IntoFunctionDefinition, StructMemoryKind,
+    };
 }
 
 /// Defines the current ABI version
@@ -48,6 +54,54 @@ pub const SET_ALLOCATOR_HANDLE_FN_NAME: &str = "set_allocator_handle";
 #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
 pub struct Guid(pub [u8; 16]);
 
+impl From<&[u8]> for Guid {
+    fn from(bytes: &[u8]) -> Self {
+        Guid(md5::compute(&bytes).0)
+    }
+}
+
+impl fmt::Display for Guid {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let hyphenated = format_hyphenated(&self.0);
+
+        // SAFETY: The encoded buffer is ASCII encoded
+        let hyphenated = unsafe { std::str::from_utf8_unchecked(&hyphenated) };
+
+        return f.write_str(hyphenated);
+
+        #[inline]
+        const fn format_hyphenated(src: &[u8; 16]) -> [u8; 36] {
+            const LUT: [u8; 16] = [
+                b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'a', b'b', b'c', b'd',
+                b'e', b'f',
+            ];
+
+            let groups = [(0, 8), (9, 13), (14, 18), (19, 23), (24, 36)];
+            let mut dst = [0; 36];
+
+            let mut group_idx = 0;
+            let mut i = 0;
+            while group_idx < 5 {
+                let (start, end) = groups[group_idx];
+                let mut j = start;
+                while j < end {
+                    let x = src[i];
+                    i += 1;
+
+                    dst[j] = LUT[(x >> 4) as usize];
+                    dst[j + 1] = LUT[(x & 0x0f) as usize];
+                    j += 2;
+                }
+                if group_idx < 4 {
+                    dst[end] = b'-';
+                }
+                group_idx += 1;
+            }
+            dst
+        }
+    }
+}
+
 /// Represents the privacy level of modules, functions, or variables.
 #[repr(u8)]
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
diff --git a/crates/mun_abi/src/module_info.rs b/crates/mun_abi/src/module_info.rs
index 162d1f626..af144022e 100644
--- a/crates/mun_abi/src/module_info.rs
+++ b/crates/mun_abi/src/module_info.rs
@@ -9,7 +9,7 @@ pub struct ModuleInfo {
     /// Module functions
     pub(crate) functions: *const FunctionDefinition,
     /// Module types
-    pub(crate) types: *const *const TypeInfo,
+    pub(crate) types: *const TypeInfo,
     /// Number of module functions
     pub num_functions: u32,
     /// Number of module types
@@ -47,13 +47,11 @@ impl ModuleInfo {
     }
 
     /// Returns the module's types.
-    pub fn types(&self) -> &[&TypeInfo] {
+    pub fn types(&self) -> &[TypeInfo] {
         if self.num_types == 0 {
             &[]
         } else {
-            unsafe {
-                slice::from_raw_parts(self.types.cast::<&TypeInfo>(), self.num_types as usize)
-            }
+            unsafe { slice::from_raw_parts(self.types, self.num_types as usize) }
         }
     }
 }
@@ -96,7 +94,7 @@ mod tests {
         let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
         let type_info = fake_type_info(&type_name, 1, 1, TypeInfoData::Primitive);
 
-        let return_type = Some(&type_info);
+        let return_type = Some(type_info.id);
         let fn_name = CString::new(FAKE_FN_NAME).expect("Invalid fake fn name.");
         let fn_prototype = fake_fn_prototype(&fn_name, &[], return_type);
 
@@ -109,10 +107,10 @@ mod tests {
         let struct_name = CString::new(FAKE_STRUCT_NAME).expect("Invalid fake struct name");
         let struct_info = fake_struct_info(&[], &[], &[], Default::default());
         let type_info = fake_type_info(&struct_name, 1, 1, TypeInfoData::Struct(struct_info));
-        let types = &[&type_info];
+        let types = [type_info];
 
         let module_path = CString::new(FAKE_MODULE_PATH).expect("Invalid fake module path.");
-        let module = fake_module_info(&module_path, functions, types);
+        let module = fake_module_info(&module_path, functions, &types);
 
         let result_functions = module.functions();
         assert_eq!(result_functions.len(), functions.len());
@@ -129,7 +127,7 @@ mod tests {
             );
         }
 
-        let result_types: &[&TypeInfo] = module.types();
+        let result_types: &[TypeInfo] = module.types();
         assert_eq!(result_types.len(), types.len());
         for (lhs, rhs) in result_types.iter().zip(types.iter()) {
             assert_eq!(lhs, rhs);
@@ -140,7 +138,7 @@ mod tests {
                 if let TypeInfoData::Struct(rhs) = &rhs.data {
                     assert_eq!(lhs.field_types(), rhs.field_types());
                 } else {
-                    assert!(false);
+                    unreachable!()
                 }
             }
         }
diff --git a/crates/mun_abi/src/static_type_map.rs b/crates/mun_abi/src/static_type_map.rs
index 6d8fccfbe..4461d5716 100644
--- a/crates/mun_abi/src/static_type_map.rs
+++ b/crates/mun_abi/src/static_type_map.rs
@@ -1,19 +1,24 @@
+//! A [`StaticTypeMap`] is a map that maps from a type to a value.
+
 use parking_lot::ReentrantMutex;
 use std::any::TypeId;
 use std::cell::RefCell;
 use std::collections::HashMap;
 
+/// A map that stores static types.
 pub struct StaticTypeMap<T: 'static> {
     map: ReentrantMutex<RefCell<HashMap<TypeId, &'static T>>>,
 }
 
-impl<T: 'static> StaticTypeMap<T> {
-    pub fn new() -> Self {
+impl<T: 'static> Default for StaticTypeMap<T> {
+    fn default() -> Self {
         Self {
             map: ReentrantMutex::new(RefCell::new(HashMap::default())),
         }
     }
+}
 
+impl<T: 'static> StaticTypeMap<T> {
     /// Initialize static value corresponding to provided type.
     ///
     /// Initialized value will stay on heap until program terminated.
diff --git a/crates/mun_abi/src/struct_info.rs b/crates/mun_abi/src/struct_info.rs
index 3503d97f2..85086b8d1 100644
--- a/crates/mun_abi/src/struct_info.rs
+++ b/crates/mun_abi/src/struct_info.rs
@@ -1,4 +1,4 @@
-use crate::TypeInfo;
+use crate::TypeId;
 use std::{ffi::CStr, os::raw::c_char, slice, str};
 
 /// Represents a struct declaration.
@@ -8,7 +8,7 @@ pub struct StructInfo {
     /// Struct fields' names
     pub field_names: *const *const c_char,
     /// Struct fields' information
-    pub(crate) field_types: *const *const TypeInfo,
+    pub(crate) field_types: *const TypeId,
     /// Struct fields' offsets
     pub(crate) field_offsets: *const u16,
     // TODO: Field accessibility levels
@@ -22,7 +22,7 @@ pub struct StructInfo {
 
 /// Represents the kind of memory management a struct uses.
 #[repr(u8)]
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum StructMemoryKind {
     /// A garbage collected struct is allocated on the heap and uses reference semantics when passed
     /// around.
@@ -50,16 +50,11 @@ impl StructInfo {
     }
 
     /// Returns the struct's field types.
-    pub fn field_types(&self) -> &[&TypeInfo] {
+    pub fn field_types(&self) -> &[TypeId] {
         if self.num_fields == 0 {
             &[]
         } else {
-            unsafe {
-                slice::from_raw_parts(
-                    self.field_types.cast::<&TypeInfo>(),
-                    self.num_fields as usize,
-                )
-            }
+            unsafe { slice::from_raw_parts(self.field_types, self.num_fields as usize) }
         }
     }
 
@@ -76,25 +71,6 @@ impl StructInfo {
     pub fn num_fields(&self) -> usize {
         self.num_fields.into()
     }
-
-    /// Returns the index of the field matching the specified `field_name`.
-    pub fn find_field_index(
-        type_name: &str,
-        struct_info: &StructInfo,
-        field_name: &str,
-    ) -> Result<usize, String> {
-        struct_info
-            .field_names()
-            .enumerate()
-            .find(|(_, name)| *name == field_name)
-            .map(|(idx, _)| idx)
-            .ok_or_else(|| {
-                format!(
-                    "Struct `{}` does not contain field `{}`.",
-                    type_name, field_name
-                )
-            })
-    }
 }
 
 impl Default for StructMemoryKind {
@@ -141,7 +117,7 @@ mod tests {
         let type_info = fake_type_info(&type_name, 1, 1, TypeInfoData::Primitive);
 
         let field_names = &[field_name.as_ptr()];
-        let field_types = &[&type_info];
+        let field_types = &[type_info.id];
         let field_offsets = &[1];
         let struct_info =
             fake_struct_info(field_names, field_types, field_offsets, Default::default());
@@ -156,7 +132,7 @@ mod tests {
     #[test]
     fn test_struct_info_memory_kind_gc() {
         let struct_memory_kind = StructMemoryKind::Gc;
-        let struct_info = fake_struct_info(&[], &[], &[], struct_memory_kind.clone());
+        let struct_info = fake_struct_info(&[], &[], &[], struct_memory_kind);
 
         assert_eq!(struct_info.memory_kind, struct_memory_kind);
     }
@@ -164,7 +140,7 @@ mod tests {
     #[test]
     fn test_struct_info_memory_kind_value() {
         let struct_memory_kind = StructMemoryKind::Value;
-        let struct_info = fake_struct_info(&[], &[], &[], struct_memory_kind.clone());
+        let struct_info = fake_struct_info(&[], &[], &[], struct_memory_kind);
 
         assert_eq!(struct_info.memory_kind, struct_memory_kind);
     }
diff --git a/crates/mun_abi/src/type_info.rs b/crates/mun_abi/src/type_info.rs
index 4385bf6bf..7ddade916 100644
--- a/crates/mun_abi/src/type_info.rs
+++ b/crates/mun_abi/src/type_info.rs
@@ -1,5 +1,6 @@
-use crate::{static_type_map::StaticTypeMap, Guid, StructInfo};
+use crate::{static_type_map::StaticTypeMap, Guid, StructInfo, TypeId};
 use once_cell::sync::OnceCell;
+use std::fmt::Debug;
 use std::{
     convert::TryInto,
     ffi::{CStr, CString},
@@ -14,10 +15,9 @@ use std::{
 /// TODO: add support for polymorphism, enumerations, type parameters, generic type definitions, and
 /// constructed generic types.
 #[repr(C)]
-#[derive(Debug)]
 pub struct TypeInfo {
-    /// Type GUID
-    pub guid: Guid,
+    /// Type ID
+    pub id: TypeId,
     /// Type name
     pub name: *const c_char,
     /// The exact size of the type in bits without any padding
@@ -28,6 +28,18 @@ pub struct TypeInfo {
     pub data: TypeInfoData,
 }
 
+impl Debug for TypeInfo {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        f.debug_struct("TypeInfo")
+            .field("id", &self.id)
+            .field("name", &self.name())
+            .field("size_in_bits", &self.size_in_bits)
+            .field("alignment", &self.alignment)
+            .field("data", &self.data)
+            .finish()
+    }
+}
+
 /// Contains data specific to a group of types that illicit the same characteristics.
 #[repr(u8)]
 #[derive(Debug)]
@@ -83,13 +95,13 @@ impl fmt::Display for TypeInfo {
 
 impl PartialEq for TypeInfo {
     fn eq(&self, other: &Self) -> bool {
-        self.guid == other.guid
+        self.id == other.id
     }
 }
 
 impl std::hash::Hash for TypeInfo {
     fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-        self.guid.hash(state);
+        self.id.hash(state);
     }
 }
 
@@ -137,7 +149,7 @@ impl<T: HasStaticTypeInfoName + 'static> HasStaticTypeInfo for *const T {
 
         let map = unsafe {
             INIT.call_once(|| {
-                VALUE = Some(StaticTypeMap::new());
+                VALUE = Some(StaticTypeMap::default());
             });
             VALUE.as_ref().unwrap()
         };
@@ -145,12 +157,12 @@ impl<T: HasStaticTypeInfoName + 'static> HasStaticTypeInfo for *const T {
         &map.call_once::<T, _>(|| {
             let name =
                 CString::new(format!("*const {}", T::type_name().to_str().unwrap())).unwrap();
-            let guid = Guid(md5::compute(&name.as_bytes()).0);
+            let guid = Guid::from(name.as_bytes());
             let name_ptr = name.as_ptr();
             (
                 name,
                 TypeInfo {
-                    guid,
+                    id: TypeId { guid },
                     name: name_ptr,
                     size_in_bits: (std::mem::size_of::<*const T>() * 8)
                         .try_into()
@@ -174,19 +186,19 @@ impl<T: HasStaticTypeInfoName + 'static> HasStaticTypeInfo for *mut T {
 
         let map = unsafe {
             INIT.call_once(|| {
-                VALUE = Some(StaticTypeMap::new());
+                VALUE = Some(StaticTypeMap::default());
             });
             VALUE.as_ref().unwrap()
         };
 
         &map.call_once::<T, _>(|| {
             let name = CString::new(format!("*mut {}", T::type_name().to_str().unwrap())).unwrap();
-            let guid = Guid(md5::compute(&name.as_bytes()).0);
+            let guid = Guid::from(name.as_bytes());
             let name_ptr = name.as_ptr();
             (
                 name,
                 TypeInfo {
-                    guid,
+                    id: TypeId { guid },
                     name: name_ptr,
                     size_in_bits: (std::mem::size_of::<*const T>() * 8)
                         .try_into()
@@ -214,9 +226,10 @@ macro_rules! impl_primitive_type_info {
                         static TYPE_INFO_NAME: OnceCell<CString> = OnceCell::new();
                         let type_info_name: &'static CString = TYPE_INFO_NAME
                             .get_or_init(|| CString::new(format!("core::{}", stringify!($ty))).unwrap());
+                        let guid = Guid::from(type_info_name.as_bytes());
 
                         TypeInfo {
-                            guid: Guid(md5::compute(&type_info_name.as_bytes()).0),
+                            id: TypeId { guid },
                             name: type_info_name.as_ptr(),
                             size_in_bits: (std::mem::size_of::<$ty>() * 8)
                                 .try_into()
@@ -250,7 +263,22 @@ macro_rules! impl_has_type_info_name {
     }
 }
 
-impl_primitive_type_info!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, f32, f64, bool);
+impl_primitive_type_info!(
+    i8,
+    i16,
+    i32,
+    i64,
+    i128,
+    u8,
+    u16,
+    u32,
+    u64,
+    u128,
+    f32,
+    f64,
+    bool,
+    ()
+);
 
 impl_has_type_info_name!(
     std::ffi::c_void => "core::void",
diff --git a/crates/mun_abi/src/type_lut.rs b/crates/mun_abi/src/type_lut.rs
new file mode 100644
index 000000000..ef975577b
--- /dev/null
+++ b/crates/mun_abi/src/type_lut.rs
@@ -0,0 +1,351 @@
+use crate::Guid;
+use itertools::izip;
+use std::ffi::CStr;
+use std::os::raw::c_char;
+use std::{ffi, fmt, slice, str};
+
+/// Represents a unique identifier for types. The runtime can use this to lookup the corresponding [`TypeInfo`].
+#[repr(C)]
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+pub struct TypeId {
+    /// The GUID of the type
+    pub guid: Guid,
+}
+
+impl From<Guid> for TypeId {
+    fn from(guid: Guid) -> Self {
+        TypeId { guid }
+    }
+}
+
+impl fmt::Display for TypeId {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.guid.fmt(f)
+    }
+}
+
+/// Represents a lookup table for type information. This is used for runtime linking.
+///
+/// Type IDs and handles are stored separately for cache efficiency.
+#[repr(C)]
+pub struct TypeLut {
+    /// Type IDs
+    pub(crate) type_ids: *const TypeId,
+    /// Type information handles
+    pub(crate) type_handles: *mut *const ffi::c_void,
+    /// Debug names
+    pub(crate) type_names: *const *const c_char,
+    /// Number of types
+    pub num_entries: u32,
+}
+
+impl TypeLut {
+    /// Returns an iterator over pairs of type IDs and type handles.
+    pub fn iter(&self) -> impl Iterator<Item = (&TypeId, &*const ffi::c_void, &str)> {
+        let (type_ids, type_ptrs, type_names) = if self.num_entries != 0 {
+            let ptrs =
+                unsafe { slice::from_raw_parts_mut(self.type_handles, self.num_entries as usize) };
+            let type_ids =
+                unsafe { slice::from_raw_parts(self.type_ids, self.num_entries as usize) };
+            let type_names =
+                unsafe { slice::from_raw_parts(self.type_names, self.num_entries as usize) };
+
+            (type_ids.iter(), ptrs.iter(), type_names.iter())
+        } else {
+            ((&[]).iter(), (&[]).iter(), (&[]).iter())
+        };
+
+        izip!(type_ids, type_ptrs, type_names).map(|(id, ptr, type_name)| {
+            (id, ptr, unsafe {
+                std::str::from_utf8_unchecked(CStr::from_ptr(*type_name).to_bytes())
+            })
+        })
+    }
+
+    /// Returns an iterator over pairs of type IDs and mutable type handles.
+    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&TypeId, &mut *const ffi::c_void, &str)> {
+        let (type_ids, type_ptrs, type_names) = if self.num_entries != 0 {
+            let ptrs =
+                unsafe { slice::from_raw_parts_mut(self.type_handles, self.num_entries as usize) };
+            let type_ids =
+                unsafe { slice::from_raw_parts(self.type_ids, self.num_entries as usize) };
+            let type_names =
+                unsafe { slice::from_raw_parts(self.type_names, self.num_entries as usize) };
+
+            (type_ids.iter(), ptrs.iter_mut(), type_names.iter())
+        } else {
+            ((&[]).iter(), (&mut []).iter_mut(), (&[]).iter())
+        };
+
+        izip!(type_ids, type_ptrs, type_names).map(|(id, ptr, type_name)| {
+            (id, ptr, unsafe {
+                std::str::from_utf8_unchecked(CStr::from_ptr(*type_name).to_bytes())
+            })
+        })
+    }
+
+    /// Returns mutable type handles.
+    pub fn type_handles_mut(&mut self) -> &mut [*const ffi::c_void] {
+        if self.num_entries == 0 {
+            &mut []
+        } else {
+            unsafe { slice::from_raw_parts_mut(self.type_handles, self.num_entries as usize) }
+        }
+    }
+
+    /// Returns type IDs.
+    pub fn type_ids(&self) -> &[TypeId] {
+        if self.num_entries == 0 {
+            &[]
+        } else {
+            unsafe { slice::from_raw_parts(self.type_ids, self.num_entries as usize) }
+        }
+    }
+
+    /// Returns a type handle, without doing bounds checking.
+    ///
+    /// This is generally not recommended, use with caution! Calling this method with an
+    /// out-of-bounds index is _undefined behavior_ even if the resulting reference is not used.
+    /// For a safe alternative see [get_ptr](#method.get_ptr).
+    ///
+    /// # Safety
+    ///
+    /// The `idx` is not bounds checked and should therefor be used with care.
+    pub unsafe fn get_type_handle_unchecked(&self, idx: u32) -> *const ffi::c_void {
+        *self.type_handles.offset(idx as isize)
+    }
+
+    /// Returns a type handle at the given index, or `None` if out of bounds.
+    pub fn get_type_handle(&self, idx: u32) -> Option<*const ffi::c_void> {
+        if idx < self.num_entries {
+            Some(unsafe { self.get_type_handle_unchecked(idx) })
+        } else {
+            None
+        }
+    }
+
+    /// Returns a mutable reference to a type handle, without doing bounds checking.
+    ///
+    /// This is generally not recommended, use with caution! Calling this method with an
+    /// out-of-bounds index is _undefined behavior_ even if the resulting reference is not used.
+    /// For a safe alternative see [get_ptr_mut](#method.get_ptr_mut).
+    ///
+    /// # Safety
+    ///
+    /// The `idx` is not bounds checked and should therefor be used with care.
+    pub unsafe fn get_type_handle_unchecked_mut(&mut self, idx: u32) -> &mut *const ffi::c_void {
+        &mut *self.type_handles.offset(idx as isize)
+    }
+
+    /// Returns a mutable reference to a type handle at the given index, or `None` if out of
+    /// bounds.
+    pub fn get_type_handle_mut(&mut self, idx: u32) -> Option<&mut *const ffi::c_void> {
+        if idx < self.num_entries {
+            Some(unsafe { self.get_type_handle_unchecked_mut(idx) })
+        } else {
+            None
+        }
+    }
+
+    /// Returns type names.
+    pub fn type_names(&self) -> impl Iterator<Item = &str> {
+        let type_names = if self.num_entries == 0 {
+            &[]
+        } else {
+            unsafe { slice::from_raw_parts(self.type_names, self.num_entries as usize) }
+        };
+
+        type_names
+            .iter()
+            .map(|n| unsafe { str::from_utf8_unchecked(CStr::from_ptr(*n).to_bytes()) })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::test_utils::{fake_type_lut, FAKE_TYPE_ID, FAKE_TYPE_NAME};
+    use std::{ffi::CString, ptr};
+
+    #[test]
+    fn test_type_lut_iter_mut_none() {
+        let type_ids = &[];
+        let type_ptrs = &mut [];
+        let type_names = &[];
+        let mut type_lut = fake_type_lut(type_ids, type_ptrs, type_names);
+
+        let iter = type_ids.iter().zip(type_ptrs.iter_mut());
+        assert_eq!(type_lut.iter_mut().count(), iter.count());
+    }
+
+    #[test]
+    fn test_type_lut_iter_mut_some() {
+        let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
+
+        let type_ids = &[FAKE_TYPE_ID];
+        let type_ptrs = &mut [ptr::null()];
+        let type_names = &[type_name.as_ptr()];
+        let mut type_lut = fake_type_lut(type_ids, type_ptrs, type_names);
+
+        let iter = type_ids.iter().zip(type_ptrs.iter_mut());
+        assert_eq!(type_lut.iter_mut().count(), iter.len());
+
+        for (lhs, rhs) in type_lut.iter_mut().zip(iter) {
+            assert_eq!(lhs.0, rhs.0);
+            assert_eq!(lhs.1, rhs.1);
+        }
+    }
+
+    #[test]
+    fn test_type_lut_ptrs_mut_none() {
+        let type_ids = &[];
+        let type_ptrs = &mut [];
+        let type_names = &[];
+        let mut type_lut = fake_type_lut(type_ids, type_ptrs, type_names);
+
+        assert_eq!(type_lut.type_handles_mut().len(), 0);
+    }
+
+    #[test]
+    fn test_type_lut_ptrs_mut_some() {
+        let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
+
+        let type_ids = &[FAKE_TYPE_ID];
+        let type_ptrs = &mut [ptr::null()];
+        let type_names = &[type_name.as_ptr()];
+        let mut type_lut = fake_type_lut(type_ids, type_ptrs, type_names);
+
+        let result = type_lut.type_handles_mut();
+        assert_eq!(result.len(), type_ptrs.len());
+        for (lhs, rhs) in result.iter().zip(type_ptrs.iter()) {
+            assert_eq!(lhs, rhs);
+        }
+    }
+
+    #[test]
+    fn test_type_lut_type_ids_none() {
+        let type_ids = &[];
+        let type_ptrs = &mut [];
+        let type_names = &[];
+        let type_lut = fake_type_lut(type_ids, type_ptrs, type_names);
+
+        assert_eq!(type_lut.type_ids().len(), 0);
+    }
+
+    #[test]
+    fn test_type_lut_type_ids_some() {
+        let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
+
+        let type_ids = &[FAKE_TYPE_ID];
+        let type_ptrs = &mut [ptr::null()];
+        let type_names = &[type_name.as_ptr()];
+        let type_lut = fake_type_lut(type_ids, type_ptrs, type_names);
+
+        let result = type_lut.type_ids();
+        assert_eq!(result.len(), type_ids.len());
+        for (lhs, rhs) in result.iter().zip(type_ids.iter()) {
+            assert_eq!(lhs, rhs);
+        }
+    }
+
+    #[test]
+    fn test_type_lut_get_ptr_unchecked() {
+        let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
+
+        let type_ids = &[FAKE_TYPE_ID];
+        let type_ptrs = &mut [ptr::null()];
+        let type_names = &[type_name.as_ptr()];
+
+        let type_lut = fake_type_lut(type_ids, type_ptrs, type_names);
+        assert_eq!(
+            unsafe { type_lut.get_type_handle_unchecked(0) },
+            type_ptrs[0]
+        );
+    }
+
+    #[test]
+    fn test_type_lut_get_ptr_none() {
+        let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
+
+        let prototype = &[FAKE_TYPE_ID];
+        let type_ptrs = &mut [ptr::null()];
+        let type_names = &[type_name.as_ptr()];
+
+        let type_lut = fake_type_lut(prototype, type_ptrs, type_names);
+        assert_eq!(type_lut.get_type_handle(1), None);
+    }
+
+    #[test]
+    fn test_type_lut_get_ptr_some() {
+        let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
+
+        let type_ids = &[FAKE_TYPE_ID];
+        let type_ptrs = &mut [ptr::null()];
+        let type_names = &[type_name.as_ptr()];
+
+        let type_lut = fake_type_lut(type_ids, type_ptrs, type_names);
+        assert_eq!(type_lut.get_type_handle(0), Some(type_ptrs[0]));
+    }
+
+    #[test]
+    fn test_type_lut_get_ptr_unchecked_mut() {
+        let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
+
+        let type_ids = &[FAKE_TYPE_ID];
+        let type_ptrs = &mut [ptr::null()];
+        let type_names = &[type_name.as_ptr()];
+
+        let mut type_lut = fake_type_lut(type_ids, type_ptrs, type_names);
+        assert_eq!(
+            unsafe { type_lut.get_type_handle_unchecked_mut(0) },
+            &mut type_ptrs[0]
+        );
+    }
+
+    #[test]
+    fn test_type_lut_get_ptr_mut_none() {
+        let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
+
+        let type_ids = &[FAKE_TYPE_ID];
+        let type_ptrs = &mut [ptr::null()];
+        let type_names = &[type_name.as_ptr()];
+
+        let mut type_lut = fake_type_lut(type_ids, type_ptrs, type_names);
+        assert_eq!(type_lut.get_type_handle_mut(1), None);
+    }
+
+    #[test]
+    fn test_type_lut_get_ptr_mut_some() {
+        let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
+
+        let type_ids = &[FAKE_TYPE_ID];
+        let type_ptrs = &mut [ptr::null()];
+        let type_names = &[type_name.as_ptr()];
+
+        let mut type_lut = fake_type_lut(type_ids, type_ptrs, type_names);
+        assert_eq!(type_lut.get_type_handle_mut(0), Some(&mut type_ptrs[0]));
+    }
+
+    #[test]
+    fn test_type_lut_type_names_none() {
+        let type_ids = &[];
+        let type_ptrs = &mut [];
+        let type_names = &[];
+        let type_lut = fake_type_lut(type_ids, type_ptrs, type_names);
+
+        assert_eq!(type_lut.type_names().count(), 0);
+    }
+
+    #[test]
+    fn test_type_lut_type_names_some() {
+        let type_name = CString::new(FAKE_TYPE_NAME).expect("Invalid fake type name.");
+
+        let type_ids = &[FAKE_TYPE_ID];
+        let type_ptrs = &mut [ptr::null()];
+        let type_names = &[type_name.as_ptr()];
+        let type_lut = fake_type_lut(type_ids, type_ptrs, type_names);
+
+        for (lhs, rhs) in type_lut.type_names().zip([FAKE_TYPE_NAME].iter()) {
+            assert_eq!(lhs, *rhs)
+        }
+    }
+}
diff --git a/crates/mun_codegen/src/code_gen/assembly_builder.rs b/crates/mun_codegen/src/code_gen/assembly_builder.rs
index a9a9f2d75..062e8f3dc 100644
--- a/crates/mun_codegen/src/code_gen/assembly_builder.rs
+++ b/crates/mun_codegen/src/code_gen/assembly_builder.rs
@@ -74,9 +74,13 @@ impl<'db, 'ink, 'ctx, 't> AssemblyBuilder<'db, 'ink, 'ctx, 't> {
         };
 
         // Build the set of dependencies
+        let direct_children = module_group
+            .iter()
+            .flat_map(|module| module.children(self.code_gen.db))
+            .collect();
         let dependencies = group_ir
             .referenced_modules
-            .iter()
+            .union(&direct_children)
             .filter_map(|&module| self.module_group_partition.group_for_module(module))
             .collect::<FxHashSet<_>>()
             .into_iter()
@@ -91,7 +95,8 @@ impl<'db, 'ink, 'ctx, 't> AssemblyBuilder<'db, 'ink, 'ctx, 't> {
         symbols::gen_reflection_ir(
             self.code_gen.db,
             &value_context,
-            &file.api,
+            &file.function_definitions,
+            &file.type_definitions,
             &group_ir.dispatch_table,
             &group_ir.type_table,
             &self.code_gen.hir_types,
diff --git a/crates/mun_codegen/src/code_gen/symbols.rs b/crates/mun_codegen/src/code_gen/symbols.rs
index 2c57b0b84..4a7336ec2 100644
--- a/crates/mun_codegen/src/code_gen/symbols.rs
+++ b/crates/mun_codegen/src/code_gen/symbols.rs
@@ -1,3 +1,4 @@
+use crate::type_info::TypeInfoData;
 use crate::{
     ir::ty::HirTypeCache,
     ir::types as ir,
@@ -11,7 +12,8 @@ use crate::{
         AsValue, CanInternalize, Global, IrValueContext, IterAsIrValue, SizedValueType, Value,
     },
 };
-use hir::{HirDatabase, Ty};
+use abi::HasStaticTypeInfo;
+use hir::HirDatabase;
 use inkwell::{attributes::Attribute, module::Linkage, types::AnyType};
 use std::convert::TryFrom;
 use std::{collections::HashSet, ffi::CString};
@@ -23,7 +25,6 @@ fn gen_prototype_from_function<'ink>(
     function: hir::Function,
     hir_types: &HirTypeCache,
 ) -> ir::FunctionPrototype<'ink> {
-    let module = context.module;
     let name = function.full_name(db);
 
     // Internalize the name of the function prototype
@@ -33,15 +34,20 @@ fn gen_prototype_from_function<'ink>(
 
     // Get the `ir::TypeInfo` pointer for the return type of the function
     let fn_sig = function.ty(db).callable_sig(db).unwrap();
-    let return_type = gen_signature_return_type(context, fn_sig.ret(), hir_types);
+    let return_type = if fn_sig.ret().is_empty() {
+        <()>::type_info().id.clone().into()
+    } else {
+        ir::TypeId {
+            guid: hir_types.type_info(fn_sig.ret()).guid,
+        }
+    };
 
     // Construct an array of pointers to `ir::TypeInfo`s for the arguments of the prototype
     let arg_types = fn_sig
         .params()
         .iter()
-        .map(|ty| {
-            TypeTable::get(module, &hir_types.type_info(ty), context)
-                .expect("expected a TypeInfo for a prototype argument but it was not found")
+        .map(|ty| ir::TypeId {
+            guid: hir_types.type_info(ty).guid,
         })
         .into_const_private_pointer_or_null(format!("fn_sig::<{}>::arg_types", &name), context);
 
@@ -60,8 +66,6 @@ fn gen_prototype_from_dispatch_entry<'ink>(
     context: &IrValueContext<'ink, '_, '_>,
     function: &DispatchableFunction,
 ) -> ir::FunctionPrototype<'ink> {
-    let module = context.module;
-
     // Internalize the name of the function prototype
     let name_str = CString::new(function.prototype.name.clone())
         .expect("function prototype name is not a valid CString")
@@ -71,17 +75,20 @@ fn gen_prototype_from_dispatch_entry<'ink>(
         );
 
     // Get the `ir::TypeInfo` pointer for the return type of the function
-    let return_type =
-        gen_signature_return_type_from_type_info(context, function.prototype.ret_type.clone());
+    let return_type = function
+        .prototype
+        .ret_type
+        .as_ref()
+        .map(|ty| ir::TypeId { guid: ty.guid })
+        .unwrap_or_else(|| <()>::type_info().id.clone().into());
 
     // Construct an array of pointers to `ir::TypeInfo`s for the arguments of the prototype
     let arg_types = function
         .prototype
         .arg_types
         .iter()
-        .map(|type_info| {
-            TypeTable::get(module, type_info, context)
-                .expect("expected a TypeInfo for a prototype argument but it was not found")
+        .map(|type_info| ir::TypeId {
+            guid: type_info.guid,
         })
         .into_const_private_pointer_or_null(
             format!("{}_param_types", function.prototype.name),
@@ -98,39 +105,120 @@ fn gen_prototype_from_dispatch_entry<'ink>(
     }
 }
 
-/// Given a function, construct a pointer to a `ir::TypeInfo` global that represents the return type
-/// of the function; or `null` if the return type is empty.
-fn gen_signature_return_type<'ink>(
+/// Construct a global that holds a reference to all types. e.g.:
+/// MunTypeInfo[] definitions = { ... }
+fn get_type_definition_array<'ink, 'a>(
+    db: &dyn HirDatabase,
     context: &IrValueContext<'ink, '_, '_>,
-    ret_type: &Ty,
+    types: impl Iterator<Item = &'a TypeInfo>,
     hir_types: &HirTypeCache,
 ) -> Value<'ink, *const ir::TypeInfo<'ink>> {
-    gen_signature_return_type_from_type_info(
-        context,
-        if ret_type.is_empty() {
-            None
-        } else {
-            Some(hir_types.type_info(ret_type))
-        },
-    )
+    types
+        .map(|type_info| ir::TypeInfo {
+            id: ir::TypeId {
+                guid: type_info.guid,
+            },
+            name: CString::new(type_info.name.clone())
+                .expect("typename is not a valid CString")
+                .intern(format!("type_info::<{}>::name", type_info.name), context)
+                .as_value(context),
+            size_in_bits: type_info
+                .size
+                .bit_size
+                .try_into()
+                .expect("could not convert size in bits to smaller size"),
+            alignment: type_info
+                .size
+                .alignment
+                .try_into()
+                .expect("could not convert alignment to smaller size"),
+            data: gen_type_info_data(db, &type_info.data, context, hir_types),
+        })
+        .into_const_private_pointer_or_null("fn.get_info.types", context)
 }
 
-/// Given a function, construct a pointer to a `ir::TypeInfo` global that represents the return type
-/// of the function; or `null` if the return type is empty.
-fn gen_signature_return_type_from_type_info<'ink>(
+fn gen_type_info_data<'ink>(
+    db: &dyn HirDatabase,
+    data: &TypeInfoData,
     context: &IrValueContext<'ink, '_, '_>,
-    ret_type: Option<TypeInfo>,
-) -> Value<'ink, *const ir::TypeInfo<'ink>> {
-    ret_type
-        .map(|info| {
-            TypeTable::get(context.module, &info, context).unwrap_or_else(|| {
-                panic!(
-                    "could not find TypeInfo that should definitely be there: {}",
-                    info.name
+    hir_types: &HirTypeCache,
+) -> ir::TypeInfoData<'ink> {
+    match data {
+        TypeInfoData::Primitive => ir::TypeInfoData::Primitive,
+        TypeInfoData::Struct(s) => {
+            ir::TypeInfoData::Struct(gen_struct_info(db, *s, context, hir_types))
+        }
+    }
+}
+
+fn gen_struct_info<'ink>(
+    db: &dyn HirDatabase,
+    hir_struct: hir::Struct,
+    context: &IrValueContext<'ink, '_, '_>,
+    hir_types: &HirTypeCache,
+) -> ir::StructInfo<'ink> {
+    let struct_ir = hir_types.get_struct_type(hir_struct);
+    let name = hir_struct.full_name(db);
+    let fields = hir_struct.fields(db);
+
+    // Construct an array of field names (or null if there are no fields)
+    let field_names = fields
+        .iter()
+        .enumerate()
+        .map(|(idx, field)| {
+            CString::new(field.name(db).to_string())
+                .expect("field name is not a valid CString")
+                .intern(
+                    format!("struct_info::<{}>::field_names.{}", name, idx),
+                    context,
                 )
-            })
+                .as_value(context)
+        })
+        .into_const_private_pointer_or_null(
+            format!("struct_info::<{}>::field_names", name),
+            context,
+        );
+
+    // Construct an array of field types (or null if there are no fields)
+    let field_types = fields
+        .iter()
+        .map(|field| {
+            let field_type_info = hir_types.type_info(&field.ty(db));
+            ir::TypeId {
+                guid: field_type_info.guid,
+            }
+        })
+        .into_const_private_pointer_or_null(
+            format!("struct_info::<{}>::field_types", name),
+            context,
+        );
+
+    // Construct an array of field offsets (or null if there are no fields)
+    let field_offsets = fields
+        .iter()
+        .enumerate()
+        .map(|(idx, _)| {
+            context
+                .type_context
+                .target_data
+                .offset_of_element(&struct_ir, idx as u32)
+                .unwrap() as u16
         })
-        .unwrap_or_else(|| Value::null(context))
+        .into_const_private_pointer_or_null(
+            format!("struct_info::<{}>::field_offsets", name),
+            context,
+        );
+
+    ir::StructInfo {
+        field_names,
+        field_types,
+        field_offsets,
+        num_fields: fields
+            .len()
+            .try_into()
+            .expect("could not convert num_fields to smaller bit size"),
+        memory_kind: hir_struct.data(db.upcast()).memory_kind,
+    }
 }
 
 /// Construct a global that holds a reference to all functions. e.g.:
@@ -169,6 +257,51 @@ fn get_function_definition_array<'ink, 'a>(
         .into_const_private_global("fn.get_info.functions", context)
 }
 
+/// Generate the type lookup table information. e.g.:
+/// ```c
+/// MunTypeLut typeLut = { ... }
+/// ```
+fn gen_type_lut<'ink>(
+    context: &IrValueContext<'ink, '_, '_>,
+    type_table: &TypeTable,
+) -> ir::TypeLut<'ink> {
+    let module = context.module;
+
+    // Get a list of all Guids
+    let type_ids = type_table
+        .entries()
+        .iter()
+        .map(|ty| ir::TypeId { guid: ty.guid })
+        .into_const_private_pointer("fn.get_info.typeLut.typeIds", context);
+
+    let type_names = type_table
+        .entries()
+        .iter()
+        .map(|ty| {
+            CString::new(ty.name.as_str())
+                .expect("unable to create CString from typeinfo name")
+                .intern(&ty.name, context)
+                .as_value(context)
+        })
+        .into_const_private_pointer("fn.get_info.typeLut.typeNames", context);
+
+    let type_ptrs = TypeTable::find_global(module)
+        .map(|type_table| {
+            Value::<*mut *const std::ffi::c_void>::with_cast(
+                type_table.as_value(context).value,
+                context,
+            )
+        })
+        .unwrap_or_else(|| Value::null(context));
+
+    ir::TypeLut {
+        type_ids,
+        type_ptrs,
+        type_names,
+        num_entries: type_table.num_types().try_into().expect("too many types"),
+    }
+}
+
 /// Generate the dispatch table information. e.g.:
 /// ```c
 /// MunDispatchTable dispatchTable = { ... }
@@ -211,22 +344,21 @@ fn gen_dispatch_table<'ink>(
 pub(super) fn gen_reflection_ir<'db, 'ink>(
     db: &'db dyn HirDatabase,
     context: &IrValueContext<'ink, '_, '_>,
-    api: &HashSet<hir::Function>,
+    function_definitions: &HashSet<hir::Function>,
+    type_definitions: &HashSet<TypeInfo>,
     dispatch_table: &DispatchTable<'ink>,
     type_table: &TypeTable<'ink>,
     hir_types: &HirTypeCache<'db, 'ink>,
     optimization_level: inkwell::OptimizationLevel,
     dependencies: Vec<String>,
 ) {
-    let module = context.module;
-
-    let num_functions = api.len() as u32;
-    let functions = get_function_definition_array(db, context, api.iter(), hir_types);
+    let num_functions = function_definitions.len() as u32;
+    let functions =
+        get_function_definition_array(db, context, function_definitions.iter(), hir_types);
 
     // Get the TypeTable global
-    let types = TypeTable::find_global(module)
-        .map(|g| g.as_value(context))
-        .unwrap_or_else(|| Value::null(context));
+    let num_types = type_definitions.len() as u32;
+    let types = get_type_definition_array(db, context, type_definitions.iter(), hir_types);
 
     // Construct the module info struct
     let module_info = ir::ModuleInfo {
@@ -237,18 +369,21 @@ pub(super) fn gen_reflection_ir<'db, 'ink>(
         functions: functions.as_value(context),
         num_functions,
         types,
-        num_types: type_table.num_types() as u32,
+        num_types,
     };
 
     // Construct the dispatch table struct
     let dispatch_table = gen_dispatch_table(context, dispatch_table);
 
+    let type_lut = gen_type_lut(context, type_table);
+
     // Construct the actual `get_info` function
     gen_get_info_fn(
         db,
         context,
         module_info,
         dispatch_table,
+        type_lut,
         optimization_level,
         dependencies,
     );
@@ -262,6 +397,7 @@ fn gen_get_info_fn<'ink>(
     context: &IrValueContext<'ink, '_, '_>,
     module_info: ir::ModuleInfo<'ink>,
     dispatch_table: ir::DispatchTable<'ink>,
+    type_lut: ir::TypeLut<'ink>,
     optimization_level: inkwell::OptimizationLevel,
     dependencies: Vec<String>,
 ) {
@@ -323,16 +459,20 @@ fn gen_get_info_fn<'ink>(
     let dispatch_table_addr = builder
         .build_struct_gep(result_ptr, 3, "dispatch_table")
         .expect("could not retrieve `dispatch_table` from result struct");
+    let type_lut_addr = builder
+        .build_struct_gep(result_ptr, 5, "type_lut")
+        .expect("could not retrieve `type_lut` from result struct");
     let dependencies_addr = builder
-        .build_struct_gep(result_ptr, 5, "dependencies")
+        .build_struct_gep(result_ptr, 7, "dependencies")
         .expect("could not retrieve `dependencies` from result struct");
     let num_dependencies_addr = builder
-        .build_struct_gep(result_ptr, 7, "num_dependencies")
+        .build_struct_gep(result_ptr, 9, "num_dependencies")
         .expect("could not retrieve `num_dependencies` from result struct");
 
     // Assign the struct values one by one.
     builder.build_store(symbols_addr, module_info.as_value(context).value);
     builder.build_store(dispatch_table_addr, dispatch_table.as_value(context).value);
+    builder.build_store(type_lut_addr, type_lut.as_value(context).value);
     builder.build_store(
         dependencies_addr,
         dependencies
diff --git a/crates/mun_codegen/src/intrinsics.rs b/crates/mun_codegen/src/intrinsics.rs
index e44d714d1..e154a6004 100644
--- a/crates/mun_codegen/src/intrinsics.rs
+++ b/crates/mun_codegen/src/intrinsics.rs
@@ -1,8 +1,5 @@
 use crate::ir::dispatch_table::FunctionPrototype;
-use crate::type_info::TypeInfo;
-use inkwell::context::Context;
-use inkwell::targets::TargetData;
-use inkwell::types::FunctionType;
+use inkwell::{context::Context, targets::TargetData, types::FunctionType};
 use std::ffi;
 
 #[macro_use]
@@ -20,5 +17,5 @@ pub trait Intrinsic: Sync {
 
 intrinsics! {
     /// Allocates memory for the specified `type` in the allocator referred to by `alloc_handle`.
-    pub fn new(type: *const TypeInfo, alloc_handle: *mut ffi::c_void) -> *const *mut ffi::c_void;
+    pub fn new(type_handle: *const ffi::c_void, alloc_handle: *mut ffi::c_void) -> *const *mut ffi::c_void;
 }
diff --git a/crates/mun_codegen/src/ir/body.rs b/crates/mun_codegen/src/ir/body.rs
index 8f19da78d..7c67f3b9e 100644
--- a/crates/mun_codegen/src/ir/body.rs
+++ b/crates/mun_codegen/src/ir/body.rs
@@ -1,9 +1,7 @@
-use crate::module_group::ModuleGroup;
 use crate::{
     intrinsics,
-    ir::ty::HirTypeCache,
-    ir::types as ir,
-    ir::{dispatch_table::DispatchTable, type_table::TypeTable},
+    ir::{dispatch_table::DispatchTable, ty::HirTypeCache, type_table::TypeTable},
+    module_group::ModuleGroup,
     value::Global,
 };
 use hir::{
@@ -11,13 +9,14 @@ use hir::{
     Literal, LogicOp, Name, Ordering, Pat, PatId, Path, ResolveBitness, Resolver, Statement,
     TyKind, UnaryOp, ValueNs,
 };
-use inkwell::values::BasicMetadataValueEnum;
 use inkwell::{
     basic_block::BasicBlock,
     builder::Builder,
     context::Context,
-    values::{AggregateValueEnum, GlobalValue, PointerValue},
-    values::{BasicValueEnum, CallSiteValue, FloatValue, FunctionValue, IntValue, StructValue},
+    values::{
+        AggregateValueEnum, BasicMetadataValueEnum, BasicValueEnum, CallSiteValue, FloatValue,
+        FunctionValue, GlobalValue, IntValue, PointerValue, StructValue,
+    },
     AddressSpace, FloatPredicate, IntPredicate,
 };
 use std::{collections::HashMap, sync::Arc};
@@ -31,7 +30,7 @@ struct LoopInfo<'ink> {
 pub(crate) struct ExternalGlobals<'ink> {
     pub alloc_handle: Option<GlobalValue<'ink>>,
     pub dispatch_table: Option<GlobalValue<'ink>>,
-    pub type_table: Option<Global<'ink, [*const ir::TypeInfo<'ink>]>>,
+    pub type_table: Option<Global<'ink, [*const std::ffi::c_void]>>,
 }
 
 pub(crate) struct BodyIrGenerator<'db, 'ink, 't> {
diff --git a/crates/mun_codegen/src/ir/file.rs b/crates/mun_codegen/src/ir/file.rs
index f18bdffc3..aef467d26 100644
--- a/crates/mun_codegen/src/ir/file.rs
+++ b/crates/mun_codegen/src/ir/file.rs
@@ -1,5 +1,6 @@
 use super::body::ExternalGlobals;
 use crate::module_group::ModuleGroup;
+use crate::type_info::TypeInfo;
 use crate::{
     code_gen::CodeGenContext,
     ir::body::BodyIrGenerator,
@@ -17,7 +18,9 @@ pub struct FileIr<'ink> {
     /// The LLVM module that contains the IR
     pub llvm_module: Module<'ink>,
     /// The `hir::Function`s that constitute the file's API.
-    pub api: HashSet<hir::Function>,
+    pub function_definitions: HashSet<hir::Function>,
+    /// The types defined in this file
+    pub type_definitions: HashSet<TypeInfo>,
 }
 
 /// Generates IR for the specified file.
@@ -33,6 +36,7 @@ pub(crate) fn gen_file_ir<'db, 'ink>(
     // Generate all exposed function and wrapper function signatures.
     // Use a `BTreeMap` to guarantee deterministically ordered output.ures
     let mut functions = HashMap::new();
+    let mut type_definitions = HashSet::new();
     let mut wrapper_functions = BTreeMap::new();
     for def in module_group
         .iter()
@@ -57,6 +61,9 @@ pub(crate) fn gen_file_ir<'db, 'ink>(
                 }
             }
         }
+        if let ModuleDef::Struct(s) = def {
+            type_definitions.insert(hir_types.type_info(&s.ty(code_gen.db)));
+        }
     }
 
     let external_globals = {
@@ -118,11 +125,15 @@ pub(crate) fn gen_file_ir<'db, 'ink>(
     }
 
     // Filter private methods
-    let api: HashSet<hir::Function> = functions
+    let function_definitions: HashSet<hir::Function> = functions
         .keys()
         .copied()
         .filter(|&f| module_group.should_export_fn(code_gen.db, f))
         .collect();
 
-    FileIr { llvm_module, api }
+    FileIr {
+        llvm_module,
+        function_definitions,
+        type_definitions,
+    }
 }
diff --git a/crates/mun_codegen/src/ir/file_group.rs b/crates/mun_codegen/src/ir/file_group.rs
index 8079a3609..faabf7458 100644
--- a/crates/mun_codegen/src/ir/file_group.rs
+++ b/crates/mun_codegen/src/ir/file_group.rs
@@ -116,7 +116,6 @@ pub(crate) fn gen_file_group_ir<'db, 'ink>(
     };
     let mut type_table_builder = TypeTableBuilder::new(
         code_gen.db,
-        code_gen.target_machine.get_target_data(),
         &value_context,
         intrinsics_map.keys(),
         &dispatch_table,
diff --git a/crates/mun_codegen/src/ir/type_table.rs b/crates/mun_codegen/src/ir/type_table.rs
index b7b76ddfb..3d15c788b 100644
--- a/crates/mun_codegen/src/ir/type_table.rs
+++ b/crates/mun_codegen/src/ir/type_table.rs
@@ -1,39 +1,37 @@
-use super::types as ir;
 use crate::{
     ir::dispatch_table::{DispatchTable, FunctionPrototype},
     ir::ty::HirTypeCache,
     type_info::{TypeInfo, TypeInfoData},
-    value::{AsValue, CanInternalize, Global, IrValueContext, IterAsIrValue, Value},
+    value::{Global, IrValueContext, IterAsIrValue, Value},
     ModuleGroup,
 };
 use hir::{Body, ExprId, HirDatabase, InferenceResult};
 use inkwell::{
-    context::Context, module::Linkage, module::Module, targets::TargetData, types::ArrayType,
-    values::PointerValue,
-};
-use std::{
-    collections::{BTreeSet, HashMap},
-    convert::TryInto,
-    ffi::CString,
-    mem,
-    sync::Arc,
+    context::Context, module::Linkage, module::Module, types::ArrayType, values::PointerValue,
 };
+use std::{collections::HashMap, convert::TryInto, mem, sync::Arc};
 
 /// A type table in IR is a list of pointers to unique type information that are used to generate
 /// function and struct information.
 #[derive(Debug, Clone, Eq, PartialEq)]
 pub struct TypeTable<'ink> {
+    entries: Vec<TypeInfo>,
     type_info_to_index: HashMap<TypeInfo, usize>,
     table_type: ArrayType<'ink>,
 }
 
 impl<'ink> TypeTable<'ink> {
     /// The name of the TypeTable's LLVM `GlobalValue`.
-    pub(crate) const NAME: &'static str = "global_type_table";
+    pub(crate) const NAME: &'static str = "global_type_lookup_table";
+
+    /// Returns a slice containing all types
+    pub fn entries(&self) -> &[TypeInfo] {
+        &self.entries
+    }
 
     /// Looks for a global symbol with the name of the TypeTable global in the specified `module`.
     /// Returns the global value if it could be found, `None` otherwise.
-    pub fn find_global(module: &Module<'ink>) -> Option<Global<'ink, [*const ir::TypeInfo<'ink>]>> {
+    pub fn find_global(module: &Module<'ink>) -> Option<Global<'ink, [*const std::ffi::c_void]>> {
         module
             .get_global(Self::NAME)
             .map(|g| unsafe { Global::from_raw(g) })
@@ -47,7 +45,7 @@ impl<'ink> TypeTable<'ink> {
         context: &'ink Context,
         builder: &inkwell::builder::Builder<'ink>,
         type_info: &TypeInfo,
-        table_ref: Option<Global<'ink, [*const ir::TypeInfo<'ink>]>>,
+        table_ref: Option<Global<'ink, [*const std::ffi::c_void]>>,
     ) -> PointerValue<'ink> {
         let table_ref = table_ref.expect("no type table defined");
 
@@ -74,17 +72,6 @@ impl<'ink> TypeTable<'ink> {
             .into_pointer_value()
     }
 
-    /// Retrieves the global `TypeInfo` IR value corresponding to `type_info`, if it exists.
-    pub fn get(
-        module: &Module<'ink>,
-        type_info: &TypeInfo,
-        context: &IrValueContext<'ink, '_, '_>,
-    ) -> Option<Value<'ink, *const ir::TypeInfo<'ink>>> {
-        module
-            .get_global(&type_info_global_name(type_info))
-            .map(|g| Value::<*const ir::TypeInfo>::with_cast(g.as_pointer_value(), context))
-    }
-
     /// Returns the number of types in the `TypeTable`.
     pub fn num_types(&self) -> usize {
         self.table_type.len() as usize
@@ -104,11 +91,10 @@ impl<'ink> TypeTable<'ink> {
 /// Used to build a `TypeTable` from HIR.
 pub(crate) struct TypeTableBuilder<'db, 'ink, 't> {
     db: &'db dyn HirDatabase,
-    target_data: TargetData,
     value_context: &'t IrValueContext<'ink, 't, 't>,
     dispatch_table: &'t DispatchTable<'ink>,
     hir_types: &'t HirTypeCache<'db, 'ink>,
-    entries: BTreeSet<TypeInfo>, // Use a `BTreeSet` to guarantee deterministically ordered output
+    entries: Vec<TypeInfo>,
     module_group: &'t ModuleGroup,
 }
 
@@ -116,7 +102,6 @@ impl<'db, 'ink, 't> TypeTableBuilder<'db, 'ink, 't> {
     /// Creates a new `TypeTableBuilder`.
     pub(crate) fn new<'f>(
         db: &'db dyn HirDatabase,
-        target_data: TargetData,
         value_context: &'t IrValueContext<'ink, '_, '_>,
         intrinsics: impl Iterator<Item = &'f FunctionPrototype>,
         dispatch_table: &'t DispatchTable<'ink>,
@@ -125,11 +110,10 @@ impl<'db, 'ink, 't> TypeTableBuilder<'db, 'ink, 't> {
     ) -> Self {
         let mut builder = Self {
             db,
-            target_data,
             value_context,
             dispatch_table,
             hir_types,
-            entries: BTreeSet::new(),
+            entries: Default::default(),
             module_group,
         };
 
@@ -150,7 +134,7 @@ impl<'db, 'ink, 't> TypeTableBuilder<'db, 'ink, 't> {
         if let TypeInfoData::Struct(hir_struct) = type_info.data {
             self.collect_struct(hir_struct);
         } else {
-            self.entries.insert(type_info);
+            self.entries.push(type_info);
         }
     }
 
@@ -213,7 +197,7 @@ impl<'db, 'ink, 't> TypeTableBuilder<'db, 'ink, 't> {
     /// Collects unique `TypeInfo` from the specified struct type.
     pub fn collect_struct(&mut self, hir_struct: hir::Struct) {
         let type_info = self.hir_types.type_info(&hir_struct.ty(self.db));
-        self.entries.insert(type_info);
+        self.entries.push(type_info);
 
         let fields = hir_struct.fields(self.db);
         for field in fields.into_iter() {
@@ -221,169 +205,38 @@ impl<'db, 'ink, 't> TypeTableBuilder<'db, 'ink, 't> {
         }
     }
 
-    fn gen_type_info(
-        &self,
-        type_info_to_ir: &mut HashMap<TypeInfo, Value<'ink, *const ir::TypeInfo<'ink>>>,
-        type_info: &TypeInfo,
-    ) -> Value<'ink, *const ir::TypeInfo<'ink>> {
-        // If there is already an entry, return that.
-        if let Some(value) = type_info_to_ir.get(type_info) {
-            return *value;
-        }
-
-        // Construct the header part of the abi::TypeInfo
-        let type_info_ir = ir::TypeInfo {
-            guid: type_info.guid,
-            name: CString::new(type_info.name.clone())
-                .expect("typename is not a valid CString")
-                .intern(
-                    format!("type_info::<{}>::name", type_info.name),
-                    self.value_context,
-                )
-                .as_value(self.value_context),
-            size_in_bits: type_info
-                .size
-                .bit_size
-                .try_into()
-                .expect("could not convert size in bits to smaller size"),
-            alignment: type_info
-                .size
-                .alignment
-                .try_into()
-                .expect("could not convert alignment to smaller size"),
-            data: self.gen_type_info_data(type_info_to_ir, &type_info.data),
-        }
-        .as_value(self.value_context);
-
-        // Build the global value for the ir::TypeInfo
-        let type_ir_name = type_info_global_name(type_info);
-        let value = type_info_ir
-            .into_const_private_global(&type_ir_name, self.value_context)
-            .as_value(self.value_context);
-
-        // Insert the value in this case, so we don't recompute and generate multiple values.
-        type_info_to_ir.insert(type_info.clone(), value);
-
-        value
-    }
-
-    fn gen_type_info_data(
-        &self,
-        type_info_to_ir: &mut HashMap<TypeInfo, Value<'ink, *const ir::TypeInfo<'ink>>>,
-        data: &TypeInfoData,
-    ) -> ir::TypeInfoData<'ink> {
-        match data {
-            TypeInfoData::Primitive => ir::TypeInfoData::Primitive,
-            TypeInfoData::Struct(s) => {
-                ir::TypeInfoData::Struct(self.gen_struct_info(type_info_to_ir, *s))
-            }
-        }
-    }
-
-    fn gen_struct_info(
-        &self,
-        type_info_to_ir: &mut HashMap<TypeInfo, Value<'ink, *const ir::TypeInfo<'ink>>>,
-        hir_struct: hir::Struct,
-    ) -> ir::StructInfo<'ink> {
-        let struct_ir = self.hir_types.get_struct_type(hir_struct);
-        let name = hir_struct.full_name(self.db);
-        let fields = hir_struct.fields(self.db);
-
-        // Construct an array of field names (or null if there are no fields)
-        let field_names = fields
-            .iter()
-            .enumerate()
-            .map(|(idx, field)| {
-                CString::new(field.name(self.db).to_string())
-                    .expect("field name is not a valid CString")
-                    .intern(
-                        format!("struct_info::<{}>::field_names.{}", name, idx),
-                        self.value_context,
-                    )
-                    .as_value(self.value_context)
-            })
-            .into_const_private_pointer_or_null(
-                format!("struct_info::<{}>::field_names", name),
-                self.value_context,
-            );
-
-        // Construct an array of field types (or null if there are no fields)
-        let field_types = fields
-            .iter()
-            .map(|field| {
-                let field_type_info = self.hir_types.type_info(&field.ty(self.db));
-                self.gen_type_info(type_info_to_ir, &field_type_info)
-            })
-            .into_const_private_pointer_or_null(
-                format!("struct_info::<{}>::field_types", name),
-                self.value_context,
-            );
-
-        // Construct an array of field offsets (or null if there are no fields)
-        let field_offsets = fields
-            .iter()
-            .enumerate()
-            .map(|(idx, _)| {
-                self.target_data
-                    .offset_of_element(&struct_ir, idx as u32)
-                    .unwrap() as u16
-            })
-            .into_const_private_pointer_or_null(
-                format!("struct_info::<{}>::field_offsets", name),
-                self.value_context,
-            );
-
-        ir::StructInfo {
-            field_names,
-            field_types,
-            field_offsets,
-            num_fields: fields
-                .len()
-                .try_into()
-                .expect("could not convert num_fields to smaller bit size"),
-            memory_kind: hir_struct.data(self.db.upcast()).memory_kind.clone(),
-        }
-    }
-
     /// Constructs a `TypeTable` from all *used* types.
     pub fn build(mut self) -> TypeTable<'ink> {
-        let mut entries = BTreeSet::new();
+        let mut entries = Vec::new();
         mem::swap(&mut entries, &mut self.entries);
 
-        let mut type_info_to_ir = HashMap::with_capacity(entries.len());
-        let mut type_info_to_index = HashMap::with_capacity(entries.len());
+        let type_info_to_index = entries
+            .iter()
+            .enumerate()
+            .map(|(idx, type_info)| (type_info.clone(), idx))
+            .collect();
 
         // Construct a list of all `ir::TypeInfo`s
-        let type_info_ptrs: Value<'ink, [*const ir::TypeInfo]> = entries
-            .into_iter()
-            .enumerate()
-            .map(|(index, type_info)| {
-                let ptr = self
-                    .gen_type_info(&mut type_info_to_ir, &type_info)
-                    .as_value(self.value_context);
-                type_info_to_index.insert(type_info, index);
-                ptr
-            })
+        let type_info_ptrs: Value<'ink, [*const std::ffi::c_void]> = entries
+            .iter()
+            .map(|_| Value::null(self.value_context))
             .into_value(self.value_context);
 
         // If there are types, introduce a special global that contains all the TypeInfos
         if !type_info_ptrs.is_empty() {
-            let _: Global<'ink, [*const ir::TypeInfo]> = type_info_ptrs.into_global(
+            let _: Global<'ink, [*const std::ffi::c_void]> = type_info_ptrs.into_global(
                 TypeTable::NAME,
                 self.value_context,
-                true,
+                false,
                 Linkage::External,
                 None,
             );
         };
 
         TypeTable {
+            entries,
             type_info_to_index,
             table_type: type_info_ptrs.get_type(),
         }
     }
 }
-
-fn type_info_global_name(type_info: &TypeInfo) -> String {
-    format!("type_info::<{}>", type_info.name)
-}
diff --git a/crates/mun_codegen/src/ir/types.rs b/crates/mun_codegen/src/ir/types.rs
index ed9b39596..1da6f6b67 100644
--- a/crates/mun_codegen/src/ir/types.rs
+++ b/crates/mun_codegen/src/ir/types.rs
@@ -32,17 +32,28 @@ impl<'ink> TransparentValue<'ink> for abi::StructMemoryKind {
     type Target = u8;
 
     fn as_target_value(&self, context: &IrValueContext<'ink, '_, '_>) -> Value<'ink, Self::Target> {
-        (self.clone() as u8).as_value(context)
+        (*self as u8).as_value(context)
     }
 
     fn as_bytes_and_ptrs(&self, _: &IrTypeContext<'ink, '_>) -> Vec<BytesOrPtr<'ink>> {
-        vec![vec![self.clone() as u8].into()]
+        vec![vec![*self as u8].into()]
     }
 }
 
 #[derive(AsValue)]
-pub struct TypeInfo<'ink> {
+pub struct TypeId {
     pub guid: abi::Guid,
+}
+
+impl From<abi::TypeId> for TypeId {
+    fn from(type_id: abi::TypeId) -> Self {
+        TypeId { guid: type_id.guid }
+    }
+}
+
+#[derive(AsValue)]
+pub struct TypeInfo<'ink> {
+    pub id: TypeId,
     pub name: Value<'ink, *const u8>,
     pub size_in_bits: u32,
     pub alignment: u8,
@@ -58,8 +69,8 @@ pub enum TypeInfoData<'ink> {
 
 #[derive(AsValue)]
 pub struct FunctionSignature<'ink> {
-    pub arg_types: Value<'ink, *const *const TypeInfo<'ink>>,
-    pub return_type: Value<'ink, *const TypeInfo<'ink>>,
+    pub arg_types: Value<'ink, *const TypeId>,
+    pub return_type: TypeId,
     pub num_arg_types: u16,
 }
 
@@ -78,7 +89,7 @@ pub struct FunctionDefinition<'ink> {
 #[derive(AsValue)]
 pub struct StructInfo<'ink> {
     pub field_names: Value<'ink, *const *const u8>,
-    pub field_types: Value<'ink, *const *const TypeInfo<'ink>>,
+    pub field_types: Value<'ink, *const TypeId>,
     pub field_offsets: Value<'ink, *const u16>,
     pub num_fields: u16,
     pub memory_kind: abi::StructMemoryKind,
@@ -88,7 +99,7 @@ pub struct StructInfo<'ink> {
 pub struct ModuleInfo<'ink> {
     pub path: Value<'ink, *const u8>,
     pub functions: Value<'ink, *const FunctionDefinition<'ink>>,
-    pub types: Value<'ink, *const *const TypeInfo<'ink>>,
+    pub types: Value<'ink, *const TypeInfo<'ink>>,
     pub num_functions: u32,
     pub num_types: u32,
 }
@@ -100,10 +111,19 @@ pub struct DispatchTable<'ink> {
     pub num_entries: u32,
 }
 
+#[derive(AsValue)]
+pub struct TypeLut<'ink> {
+    pub type_ids: Value<'ink, *const TypeId>,
+    pub type_ptrs: Value<'ink, *mut *const std::ffi::c_void>,
+    pub type_names: Value<'ink, *const *const u8>,
+    pub num_entries: u32,
+}
+
 #[derive(AsValue)]
 pub struct AssemblyInfo<'ink> {
     pub symbols: ModuleInfo<'ink>,
     pub dispatch_table: DispatchTable<'ink>,
+    pub type_lut: TypeLut<'ink>,
     pub dependencies: Value<'ink, *const *const u8>,
     pub num_dependencies: u32,
 }
diff --git a/crates/mun_codegen/src/type_info.rs b/crates/mun_codegen/src/type_info.rs
index c5232c757..854d834fe 100644
--- a/crates/mun_codegen/src/type_info.rs
+++ b/crates/mun_codegen/src/type_info.rs
@@ -74,7 +74,7 @@ impl TypeInfo {
     pub fn new_primitive<S: AsRef<str>>(name: S, type_size: TypeSize) -> TypeInfo {
         TypeInfo {
             name: name.as_ref().to_string(),
-            guid: Guid(md5::compute(name.as_ref()).0),
+            guid: Guid::from(name.as_ref().as_bytes()),
             size: type_size,
             data: TypeInfoData::Primitive,
         }
@@ -102,7 +102,7 @@ impl TypeInfo {
             )
         };
         Self {
-            guid: Guid(md5::compute(&guid_string).0),
+            guid: Guid::from(guid_string.as_bytes()),
             name,
             size: type_size,
             data: TypeInfoData::Struct(s),
diff --git a/crates/mun_codegen/src/value/array_value.rs b/crates/mun_codegen/src/value/array_value.rs
index 2e924283e..7f8ff892f 100644
--- a/crates/mun_codegen/src/value/array_value.rs
+++ b/crates/mun_codegen/src/value/array_value.rs
@@ -85,6 +85,8 @@ macro_rules! impl_array(
                     Value::from_raw(value)
                 }
             }
+
+            impl<'ink, T> AddressableType<'ink, [T; $size]> for [T; $size] {}
         )+
     }
 );
diff --git a/crates/mun_codegen/src/value/pointer_value.rs b/crates/mun_codegen/src/value/pointer_value.rs
index ead8d7725..0d110b98f 100644
--- a/crates/mun_codegen/src/value/pointer_value.rs
+++ b/crates/mun_codegen/src/value/pointer_value.rs
@@ -2,8 +2,33 @@ use super::{
     AddressableType, AsBytesAndPtrs, BytesOrPtr, ConcreteValueType, HasConstValue, IrTypeContext,
     IrValueContext, PointerValueType, SizedValueType, Value,
 };
+use crate::value::ValueType;
 use inkwell::{types::PointerType, AddressSpace};
 
+impl<'ink> ConcreteValueType<'ink> for *const std::ffi::c_void {
+    type Value = inkwell::values::PointerValue<'ink>;
+}
+
+impl<'ink> SizedValueType<'ink> for *const std::ffi::c_void {
+    fn get_ir_type(
+        context: &IrTypeContext<'ink, '_>,
+    ) -> <<Self as ConcreteValueType<'ink>>::Value as ValueType<'ink>>::Type {
+        context
+            .context
+            .ptr_sized_int_type(context.target_data, None)
+            .ptr_type(AddressSpace::Generic)
+    }
+}
+
+impl<'ink> PointerValueType<'ink> for *const std::ffi::c_void {
+    fn get_ptr_type(
+        context: &IrTypeContext<'ink, '_>,
+        address_space: Option<AddressSpace>,
+    ) -> PointerType<'ink> {
+        Self::get_ir_type(context).ptr_type(address_space.unwrap_or(AddressSpace::Generic))
+    }
+}
+
 impl<'ink, T: PointerValueType<'ink>> ConcreteValueType<'ink> for *const T {
     type Value = inkwell::values::PointerValue<'ink>;
 }
diff --git a/crates/mun_language_server/Cargo.toml b/crates/mun_language_server/Cargo.toml
index 22fb70d0a..3bc97b040 100644
--- a/crates/mun_language_server/Cargo.toml
+++ b/crates/mun_language_server/Cargo.toml
@@ -21,7 +21,7 @@ serde = "1.0"
 serde_json = "1.0"
 serde_derive = "1.0"
 anyhow = "1.0"
-thiserror = "1.0"
+thiserror = "1.0.19"
 salsa = "0.16.1"
 hir = { version = "=0.3.0", path="../mun_hir", package="mun_hir" }
 threadpool="1.8.1"
diff --git a/crates/mun_memory/Cargo.toml b/crates/mun_memory/Cargo.toml
index 0d1d59e4b..981cc0c09 100644
--- a/crates/mun_memory/Cargo.toml
+++ b/crates/mun_memory/Cargo.toml
@@ -14,9 +14,12 @@ categories = ["game-development", "mun"]
 
 [dependencies]
 abi = { version = "=0.3.0", path = "../mun_abi", package = "mun_abi" }
+itertools = "0.10.3"
+lazy_static = "1.4.0"
 once_cell = "1.4.0"
 parking_lot = "0.12.0"
-lazy_static = "1.4.0"
+rustc-hash = "1.1"
+thiserror = "1.0.19"
 
 [dev-dependencies]
 paste = "1.0"
diff --git a/crates/mun_memory/src/cast.rs b/crates/mun_memory/src/cast.rs
index 47c0a2c7e..6b7cbd9d8 100644
--- a/crates/mun_memory/src/cast.rs
+++ b/crates/mun_memory/src/cast.rs
@@ -7,14 +7,14 @@ type CastFn = fn(NonNull<u8>, NonNull<u8>);
 macro_rules! insert_cast_fn {
     { $table:ident, $A:ty, $B:ty } => {
         $table.insert(
-            (<$A>::type_info().guid, <$B>::type_info().guid),
+            (<$A>::type_info().id.clone(), <$B>::type_info().id.clone()),
             cast_from_to::<$A, $B> as CastFn,
         )
     }
 }
 
 lazy_static! {
-    static ref CAST_FN_TABLE: HashMap<(abi::Guid, abi::Guid), CastFn> = {
+    static ref CAST_FN_TABLE: HashMap<(abi::TypeId, abi::TypeId), CastFn> = {
         let mut table = HashMap::new();
         insert_cast_fn!(table, f32, f64);
         insert_cast_fn!(table, i8, i16);
@@ -60,12 +60,12 @@ where
 }
 
 pub fn try_cast_from_to(
-    old_guid: abi::Guid,
-    new_guid: abi::Guid,
+    old_id: abi::TypeId,
+    new_id: abi::TypeId,
     src: NonNull<u8>,
     dest: NonNull<u8>,
 ) -> bool {
-    if let Some(cast_fn) = CAST_FN_TABLE.get(&(old_guid, new_guid)) {
+    if let Some(cast_fn) = CAST_FN_TABLE.get(&(old_id, new_id)) {
         cast_fn(src, dest);
         true
     } else {
@@ -85,8 +85,8 @@ mod tests {
         B: PartialEq + std::fmt::Debug + HasStaticTypeInfo,
     {
         assert!(try_cast_from_to(
-            A::type_info().guid,
-            B::type_info().guid,
+            A::type_info().id.clone(),
+            B::type_info().id.clone(),
             unsafe { NonNull::new_unchecked(&a as *const _ as *mut _) },
             unsafe { NonNull::new_unchecked(&mut b as *mut _) }.cast::<u8>(),
         ));
@@ -95,7 +95,7 @@ mod tests {
 
     #[test]
     fn cast_f32_to_f64() {
-        assert_cast(3.14f32, 0f64);
+        assert_cast(std::f32::consts::PI, 0f64);
     }
 
     #[test]
diff --git a/crates/mun_memory/src/diff.rs b/crates/mun_memory/src/diff.rs
index 7af7c6e70..f762e72d8 100644
--- a/crates/mun_memory/src/diff.rs
+++ b/crates/mun_memory/src/diff.rs
@@ -1,6 +1,11 @@
 pub mod myers;
 
-use crate::{TypeDesc, TypeFields, TypeGroup};
+use std::sync::Arc;
+
+use crate::{
+    type_info::{FieldInfo, TypeInfo},
+    TypeFields,
+};
 
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub enum FieldEditKind {
@@ -71,11 +76,8 @@ impl PartialOrd for Diff {
     }
 }
 
-/// Given an `old` and a `new` set of types `T`, calculates the difference.
-pub fn diff<T>(old: &[T], new: &[T]) -> Vec<Diff>
-where
-    T: Copy + Eq + TypeDesc + TypeFields<T>,
-{
+/// Given an `old` and a `new` set of types, calculates the difference.
+pub fn diff(old: &[Arc<TypeInfo>], new: &[Arc<TypeInfo>]) -> Vec<Diff> {
     let diff = myers::diff(old, new);
     let mut mapping: Vec<Diff> = Vec::with_capacity(diff.len());
     let (deletions, insertions) = myers::split_diff(&diff);
@@ -84,23 +86,23 @@ where
     // compared separately.
     let deleted_primitives = deletions
         .iter()
-        .filter(|idx| unsafe { old.get_unchecked(**idx) }.group() == TypeGroup::Primitive)
+        .filter(|idx| unsafe { old.get_unchecked(**idx) }.data.is_primitive())
         .cloned()
         .collect();
     let deleted_structs = deletions
         .iter()
-        .filter(|idx| unsafe { old.get_unchecked(**idx) }.group() == TypeGroup::Struct)
+        .filter(|idx| unsafe { old.get_unchecked(**idx) }.data.is_struct())
         .cloned()
         .collect();
 
     let inserted_primitives = insertions
         .iter()
-        .filter(|idx| unsafe { new.get_unchecked(**idx) }.group() == TypeGroup::Primitive)
+        .filter(|idx| unsafe { new.get_unchecked(**idx) }.data.is_primitive())
         .cloned()
         .collect();
     let inserted_structs = insertions
         .iter()
-        .filter(|idx| unsafe { new.get_unchecked(**idx) }.group() == TypeGroup::Struct)
+        .filter(|idx| unsafe { new.get_unchecked(**idx) }.data.is_struct())
         .cloned()
         .collect();
 
@@ -119,15 +121,13 @@ where
     mapping
 }
 
-fn append_primitive_mapping<T>(
-    old: &[T],
-    new: &[T],
+fn append_primitive_mapping(
+    old: &[Arc<TypeInfo>],
+    new: &[Arc<TypeInfo>],
     deletions: Vec<usize>,
     insertions: Vec<usize>,
     mapping: &mut Vec<Diff>,
-) where
-    T: Eq,
-{
+) {
     let mut insertions: Vec<Option<usize>> = insertions.into_iter().map(Some).collect();
 
     // For all deletions,
@@ -160,18 +160,42 @@ fn append_primitive_mapping<T>(
     }
 }
 
-/// Given a set of indices for `deletions` from the `old` slice of types `T` and a set of indices
-/// for `insertions` into the `new` slice of types `T`, appends the corresponding `Diff` mapping
+/// A helper struct to check equality between fields.
+#[derive(Eq, PartialEq)]
+struct UniqueFieldInfo<'a> {
+    name: &'a str,
+    type_info: &'a TypeInfo,
+}
+
+impl<'a> From<&'a FieldInfo> for UniqueFieldInfo<'a> {
+    fn from(other: &'a FieldInfo) -> Self {
+        Self {
+            name: &other.name,
+            type_info: &other.type_info,
+        }
+    }
+}
+
+/// Given a set of indices for `deletions` from the `old` slice of types and a set of indices
+/// for `insertions` into the `new` slice of types, appends the corresponding `Diff` mapping
 /// for all
-fn append_struct_mapping<T>(
-    old: &[T],
-    new: &[T],
+fn append_struct_mapping(
+    old: &[Arc<TypeInfo>],
+    new: &[Arc<TypeInfo>],
     deletions: Vec<usize>,
     insertions: Vec<usize>,
     mapping: &mut Vec<Diff>,
-) where
-    T: Eq + TypeDesc + TypeFields<T>,
-{
+) {
+    let old_fields: Vec<Vec<UniqueFieldInfo>> = old
+        .iter()
+        .map(|ty| ty.fields().iter().map(UniqueFieldInfo::from).collect())
+        .collect();
+
+    let new_fields: Vec<Vec<UniqueFieldInfo>> = new
+        .iter()
+        .map(|ty| ty.fields().iter().map(UniqueFieldInfo::from).collect())
+        .collect();
+
     let num_deleted = deletions.len();
     let num_inserted = insertions.len();
     // For all (insertion, deletion) pairs, calculate their `myers::diff_length`
@@ -179,13 +203,14 @@ fn append_struct_mapping<T>(
         .iter()
         .flat_map(|new_idx| {
             let new_ty = unsafe { new.get_unchecked(*new_idx) };
-            let new_fields = new_ty.fields();
+            let new_fields = unsafe { new_fields.get_unchecked(*new_idx) };
             deletions
                 .iter()
                 .map(|old_idx| {
                     let old_ty = unsafe { old.get_unchecked(*old_idx) };
-                    let old_fields = old_ty.fields();
-                    let length = myers::diff_length(&old_fields, &new_fields);
+                    let old_fields = unsafe { old_fields.get_unchecked(*old_idx) };
+
+                    let length = myers::diff_length(old_fields, new_fields);
 
                     // Given N old fields and M new fields, the smallest set capable of
                     // completely changing a struct is N + M.
@@ -196,7 +221,7 @@ fn append_struct_mapping<T>(
                     // let min = new_fields.len() + old_fields.len();
 
                     // If the type's name is equal
-                    if old_ty.name() == new_ty.name() || length == 0 {
+                    if old_ty.name == new_ty.name || length == 0 {
                         // TODO: Potentially we want to retain an X% for types with equal names,
                         // whilst allowing types with different names to be modified for up to Y%.
                         length
@@ -235,13 +260,13 @@ fn append_struct_mapping<T>(
                 new_index,
             }
         } else {
-            let old_ty = unsafe { old.get_unchecked(old_index) };
-            let new_ty = unsafe { new.get_unchecked(new_index) };
+            let old_fields = unsafe { old_fields.get_unchecked(old_index) };
+            let new_fields = unsafe { new_fields.get_unchecked(new_index) };
 
             // ASSUMPTION: Don't use recursion, because all types are individually checked for
             // differences.
             // TODO: Support value struct vs heap struct?
-            let diff = field_diff(&old_ty.fields(), &new_ty.fields());
+            let diff = field_diff(old_fields, new_fields);
 
             // Edit the struct, potentially moving it in the process.
             Diff::Edit {
@@ -285,10 +310,7 @@ fn append_struct_mapping<T>(
 }
 
 /// Given an `old` and a `new` set of fields, calculates the difference.
-fn field_diff<T>(old: &[(&str, T)], new: &[(&str, T)]) -> Vec<FieldDiff>
-where
-    T: Eq,
-{
+fn field_diff<'a, 'b>(old: &[UniqueFieldInfo<'a>], new: &[UniqueFieldInfo<'b>]) -> Vec<FieldDiff> {
     let diff = myers::diff(old, new);
     let (deletions, insertions) = myers::split_diff(&diff);
     let mut insertions: Vec<Option<usize>> = insertions.into_iter().map(Some).collect();
@@ -318,7 +340,7 @@ where
         for insertion in insertions.iter_mut() {
             if let Some(new_idx) = insertion {
                 let new_ty = unsafe { new.get_unchecked(*new_idx) };
-                if old_ty.0 == new_ty.0 {
+                if old_ty.name == new_ty.name {
                     // If so,
                     mapping.push(if old_idx == *new_idx {
                         // convert the type in-place.
@@ -339,13 +361,13 @@ where
                 }
             }
         }
-        // Else, is there an insertion with a different name but same type `T`?
-        // As there can be multiple fields with the same type `T`, we want to find the closest one.
+        // Else, is there an insertion with a different name but same type?
+        // As there can be multiple fields with the same type, we want to find the closest one.
         let mut closest = None;
         for (insert_idx, insertion) in insertions.iter_mut().enumerate() {
             if let Some(new_idx) = insertion {
                 let new_ty = unsafe { new.get_unchecked(*new_idx) };
-                if old_ty.1 == new_ty.1 {
+                if old_ty.type_info == new_ty.type_info {
                     let diff = old_idx.max(*new_idx) - old_idx.min(*new_idx);
                     // If so, select the closest candidate.
                     if let Some((closest_idx, closest_diff)) = &mut closest {
diff --git a/crates/mun_memory/src/gc.rs b/crates/mun_memory/src/gc.rs
index 90639d9ba..d12b85d24 100644
--- a/crates/mun_memory/src/gc.rs
+++ b/crates/mun_memory/src/gc.rs
@@ -2,8 +2,8 @@ mod mark_sweep;
 mod ptr;
 mod root_ptr;
 
-use crate::TypeMemory;
-use std::marker::PhantomData;
+use crate::type_info::TypeInfo;
+use std::{marker::PhantomData, sync::Arc};
 
 pub use mark_sweep::MarkSweep;
 pub use ptr::{GcPtr, HasIndirectionPtr, RawGcPtr};
@@ -24,12 +24,12 @@ pub trait TypeTrace: Send + Sync {
 }
 
 /// An object that can be used to allocate and collect memory.
-pub trait GcRuntime<T: TypeMemory + TypeTrace>: Send + Sync {
+pub trait GcRuntime: Send + Sync {
     /// Allocates an object of the given type returning a GcPtr
-    fn alloc(&self, ty: T) -> GcPtr;
+    fn alloc(&self, ty: &Arc<TypeInfo>) -> GcPtr;
 
     /// Returns the type of the specified `obj`.
-    fn ptr_type(&self, obj: GcPtr) -> T;
+    fn ptr_type(&self, obj: GcPtr) -> Arc<TypeInfo>;
 
     /// Roots the specified `obj`, which keeps it and objects it references alive. Objects marked
     /// as root, must call `unroot` before they can be collected. An object can be rooted multiple
diff --git a/crates/mun_memory/src/gc/mark_sweep.rs b/crates/mun_memory/src/gc/mark_sweep.rs
index de8dd02bd..7ba7fa494 100644
--- a/crates/mun_memory/src/gc/mark_sweep.rs
+++ b/crates/mun_memory/src/gc/mark_sweep.rs
@@ -1,34 +1,77 @@
 use crate::{
     cast,
-    gc::{Event, GcPtr, GcRuntime, Observer, RawGcPtr, Stats, TypeTrace},
+    gc::{Event, GcPtr, GcRuntime, HasIndirectionPtr, Observer, RawGcPtr, Stats, TypeTrace},
     mapping::{self, FieldMapping, MemoryMapper},
-    TypeDesc, TypeGroup, TypeMemory,
+    type_info::{TypeInfo, TypeInfoData},
 };
 use mapping::{Conversion, Mapping};
 use parking_lot::RwLock;
 use std::{
     collections::{HashMap, VecDeque},
-    hash::Hash,
     ops::Deref,
     pin::Pin,
     ptr::NonNull,
+    sync::Arc,
 };
 
+pub struct Trace {
+    obj: GcPtr,
+    ty: Arc<TypeInfo>,
+    index: usize,
+}
+
+impl Iterator for Trace {
+    type Item = GcPtr;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let struct_ty = self.ty.as_ref().as_struct()?;
+        let field_count = struct_ty.fields.len();
+        while self.index < field_count {
+            let index = self.index;
+            self.index += 1;
+
+            let field = &struct_ty.fields[index];
+            if let TypeInfoData::Struct(field_struct_ty) = &field.type_info.data {
+                if field_struct_ty.memory_kind == abi::StructMemoryKind::Gc {
+                    return Some(unsafe {
+                        *self
+                            .obj
+                            .deref::<u8>()
+                            .add(usize::from(field.offset))
+                            .cast::<GcPtr>()
+                    });
+                }
+            }
+        }
+        None
+    }
+}
+
+impl TypeTrace for Arc<TypeInfo> {
+    type Trace = Trace;
+
+    fn trace(&self, obj: GcPtr) -> Self::Trace {
+        Trace {
+            ty: self.clone(),
+            obj,
+            index: 0,
+        }
+    }
+}
+
 /// Implements a simple mark-sweep type garbage collector.
 #[derive(Debug)]
-pub struct MarkSweep<T, O>
+pub struct MarkSweep<O>
 where
-    T: TypeMemory + TypeTrace + Clone,
     O: Observer<Event = Event>,
 {
-    objects: RwLock<HashMap<GcPtr, Pin<Box<ObjectInfo<T>>>>>,
+    objects: RwLock<HashMap<GcPtr, Pin<Box<ObjectInfo>>>>,
     observer: O,
     stats: RwLock<Stats>,
 }
 
-impl<T, O> Default for MarkSweep<T, O>
+impl<O> Default for MarkSweep<O>
 where
-    T: TypeMemory + TypeTrace + Clone,
     O: Observer<Event = Event> + Default,
 {
     fn default() -> Self {
@@ -40,9 +83,8 @@ where
     }
 }
 
-impl<T, O> MarkSweep<T, O>
+impl<O> MarkSweep<O>
 where
-    T: TypeMemory + TypeTrace + Clone,
     O: Observer<Event = Event>,
 {
     /// Creates a `MarkSweep` memory collector with the specified `Observer`.
@@ -55,10 +97,10 @@ where
     }
 
     /// Logs an allocation
-    fn log_alloc(&self, handle: GcPtr, ty: T) {
+    fn log_alloc(&self, handle: GcPtr, ty: &TypeInfo) {
         {
             let mut stats = self.stats.write();
-            stats.allocated_memory += ty.layout().size();
+            stats.allocated_memory += ty.layout.size();
         }
 
         self.observer.event(Event::Allocation(handle));
@@ -70,8 +112,8 @@ where
     }
 }
 
-fn alloc_obj<T: Clone + TypeMemory + TypeTrace>(ty: T) -> Pin<Box<ObjectInfo<T>>> {
-    let ptr = unsafe { std::alloc::alloc(ty.layout()) };
+fn alloc_obj(ty: Arc<TypeInfo>) -> Pin<Box<ObjectInfo>> {
+    let ptr = unsafe { std::alloc::alloc(ty.layout) };
     Box::pin(ObjectInfo {
         ptr,
         ty,
@@ -80,12 +122,11 @@ fn alloc_obj<T: Clone + TypeMemory + TypeTrace>(ty: T) -> Pin<Box<ObjectInfo<T>>
     })
 }
 
-impl<T, O> GcRuntime<T> for MarkSweep<T, O>
+impl<O> GcRuntime for MarkSweep<O>
 where
-    T: TypeMemory + TypeTrace + Clone,
     O: Observer<Event = Event>,
 {
-    fn alloc(&self, ty: T) -> GcPtr {
+    fn alloc(&self, ty: &Arc<TypeInfo>) -> GcPtr {
         let object = alloc_obj(ty.clone());
 
         // We want to return a pointer to the `ObjectInfo`, to be used as handle.
@@ -100,11 +141,11 @@ where
         handle
     }
 
-    fn ptr_type(&self, handle: GcPtr) -> T {
+    fn ptr_type(&self, handle: GcPtr) -> Arc<TypeInfo> {
         let _lock = self.objects.read();
 
         // Convert the handle to our internal representation
-        let object_info: *const ObjectInfo<T> = handle.into();
+        let object_info: *const ObjectInfo = handle.into();
 
         // Return the type of the object
         unsafe { (*object_info).ty.clone() }
@@ -114,7 +155,7 @@ where
         let _lock = self.objects.write();
 
         // Convert the handle to our internal representation
-        let object_info: *mut ObjectInfo<T> = handle.into();
+        let object_info: *mut ObjectInfo = handle.into();
 
         unsafe { (*object_info).roots += 1 };
     }
@@ -123,7 +164,7 @@ where
         let _lock = self.objects.write();
 
         // Convert the handle to our internal representation
-        let object_info: *mut ObjectInfo<T> = handle.into();
+        let object_info: *mut ObjectInfo = handle.into();
 
         unsafe { (*object_info).roots -= 1 };
     }
@@ -133,9 +174,8 @@ where
     }
 }
 
-impl<T, O> MarkSweep<T, O>
+impl<O> MarkSweep<O>
 where
-    T: TypeMemory + TypeTrace + Clone,
     O: Observer<Event = Event>,
 {
     /// Collects all memory that is no longer referenced by rooted objects. Returns `true` if memory
@@ -150,7 +190,7 @@ where
             .iter()
             .filter_map(|(_, obj)| {
                 if obj.roots > 0 {
-                    Some(obj.as_ref().get_ref() as *const _ as *mut ObjectInfo<T>)
+                    Some(obj.as_ref().get_ref() as *const _ as *mut ObjectInfo)
                 } else {
                     None
                 }
@@ -167,7 +207,7 @@ where
                     .get_mut(&reference)
                     .expect("found invalid reference");
                 if ref_ptr.color == Color::White {
-                    let ptr = ref_ptr.as_ref().get_ref() as *const _ as *mut ObjectInfo<T>;
+                    let ptr = ref_ptr.as_ref().get_ref() as *const _ as *mut ObjectInfo;
                     unsafe { (*ptr).color = Color::Gray };
                     roots.push_back(ptr);
                 }
@@ -188,11 +228,11 @@ where
                 }
                 true
             } else {
-                unsafe { std::alloc::dealloc(obj.ptr, obj.ty.layout()) };
+                unsafe { std::alloc::dealloc(obj.ptr, obj.ty.layout) };
                 self.observer.event(Event::Deallocation(*h));
                 {
                     let mut stats = self.stats.write();
-                    stats.allocated_memory -= obj.ty.layout().size();
+                    stats.allocated_memory -= obj.ty.layout.size();
                 }
                 false
             }
@@ -205,12 +245,11 @@ where
     }
 }
 
-impl<T, O> MemoryMapper<T> for MarkSweep<T, O>
+impl<O> MemoryMapper for MarkSweep<O>
 where
-    T: TypeDesc + TypeMemory + TypeTrace + Clone + Eq + Hash,
     O: Observer<Event = Event>,
 {
-    fn map_memory(&self, mapping: Mapping<T, T>) -> Vec<GcPtr> {
+    fn map_memory(&self, mapping: Mapping) -> Vec<GcPtr> {
         let mut objects = self.objects.write();
 
         // Determine which types are still allocated with deleted types
@@ -246,7 +285,7 @@ where
                 if object_info.ty == *old_ty {
                     let src = unsafe { NonNull::new_unchecked(object_info.ptr) };
                     let dest = unsafe {
-                        NonNull::new_unchecked(std::alloc::alloc_zeroed(conversion.new_ty.layout()))
+                        NonNull::new_unchecked(std::alloc::alloc_zeroed(conversion.new_ty.layout))
                     };
 
                     map_fields(
@@ -258,7 +297,7 @@ where
                         dest,
                     );
 
-                    unsafe { std::alloc::dealloc(src.as_ptr(), old_ty.layout()) };
+                    unsafe { std::alloc::dealloc(src.as_ptr(), old_ty.layout) };
 
                     object_info.set(ObjectInfo {
                         ptr: dest.as_ptr(),
@@ -279,20 +318,19 @@ where
             let handle = (object.as_ref().deref() as *const _ as RawGcPtr).into();
             objects.insert(handle, object);
 
-            self.log_alloc(handle, ty);
+            self.log_alloc(handle, &ty);
         }
 
         return deleted;
 
-        fn map_fields<T, O>(
-            gc: &MarkSweep<T, O>,
-            new_allocations: &mut Vec<Pin<Box<ObjectInfo<T>>>>,
-            conversions: &HashMap<T, Conversion<T>>,
-            mapping: &[FieldMapping<T>],
+        fn map_fields<O>(
+            gc: &MarkSweep<O>,
+            new_allocations: &mut Vec<Pin<Box<ObjectInfo>>>,
+            conversions: &HashMap<Arc<TypeInfo>, Conversion>,
+            mapping: &[FieldMapping],
             src: NonNull<u8>,
             dest: NonNull<u8>,
         ) where
-            T: TypeDesc + TypeMemory + TypeTrace + Clone + Eq + Hash,
             O: Observer<Event = Event>,
         {
             for FieldMapping {
@@ -315,12 +353,12 @@ where
                             src as *mut u8
                         };
 
-                        if old_ty.group() == TypeGroup::Struct {
-                            debug_assert_eq!(new_ty.group(), TypeGroup::Struct);
+                        if old_ty.data.is_struct() {
+                            debug_assert!(new_ty.data.is_struct());
 
                             // When the name is the same, we are dealing with the same struct,
                             // but different internals
-                            let is_same_struct = old_ty.name() == new_ty.name();
+                            let is_same_struct = old_ty.name == new_ty.name;
 
                             // If the same struct changed, there must also be a conversion
                             let conversion = conversions.get(old_ty);
@@ -365,7 +403,7 @@ where
                                             std::ptr::write_bytes(
                                                 (*object).ptr,
                                                 0,
-                                                new_ty.layout().size(),
+                                                new_ty.layout.size(),
                                             )
                                         };
                                     }
@@ -396,7 +434,7 @@ where
 
                                     // Zero-initialize heap-allocated object
                                     unsafe {
-                                        std::ptr::write_bytes(object.ptr, 0, new_ty.layout().size())
+                                        std::ptr::write_bytes(object.ptr, 0, new_ty.layout.size())
                                     };
 
                                     // Write handle to field
@@ -413,7 +451,7 @@ where
                                 // Convert the handle to our internal representation
                                 // Safety: we already hold a write lock on `objects`, so
                                 // this is legal.
-                                let obj: *mut ObjectInfo<T> = field_handle.into();
+                                let obj: *mut ObjectInfo = field_handle.into();
                                 let obj = unsafe { &*obj };
 
                                 if is_same_struct {
@@ -437,7 +475,7 @@ where
                                             std::ptr::copy_nonoverlapping(
                                                 obj.ptr,
                                                 field_dest,
-                                                obj.ty.layout().size(),
+                                                obj.ty.layout.size(),
                                             )
                                         };
                                     }
@@ -446,8 +484,8 @@ where
                                 }
                             }
                         } else if !cast::try_cast_from_to(
-                            *old_ty.guid(),
-                            *new_ty.guid(),
+                            old_ty.id.clone(),
+                            new_ty.id.clone(),
                             unsafe { NonNull::new_unchecked(field_src) },
                             unsafe { NonNull::new_unchecked(field_dest) },
                         ) {
@@ -465,7 +503,7 @@ where
                             std::ptr::copy_nonoverlapping(
                                 field_src,
                                 field_dest,
-                                new_ty.layout().size(),
+                                new_ty.layout.size(),
                             )
                         };
                     }
@@ -478,7 +516,7 @@ where
                             let handle = (object.as_ref().deref() as *const _ as RawGcPtr).into();
 
                             // Zero-initialize heap-allocated object
-                            unsafe { std::ptr::write_bytes(object.ptr, 0, new_ty.layout().size()) };
+                            unsafe { std::ptr::write_bytes(object.ptr, 0, new_ty.layout.size()) };
 
                             // Write handle to field
                             let field_dest = field_dest.cast::<GcPtr>();
@@ -514,37 +552,37 @@ enum Color {
 /// meta information.
 #[derive(Debug)]
 #[repr(C)]
-struct ObjectInfo<T: TypeMemory + TypeTrace + Clone> {
+struct ObjectInfo {
     pub ptr: *mut u8,
     pub roots: u32,
     pub color: Color,
-    pub ty: T,
+    pub ty: Arc<TypeInfo>,
 }
 
 /// An `ObjectInfo` is thread-safe.
-unsafe impl<T: TypeMemory + TypeTrace + Clone> Send for ObjectInfo<T> {}
-unsafe impl<T: TypeMemory + TypeTrace + Clone> Sync for ObjectInfo<T> {}
+unsafe impl Send for ObjectInfo {}
+unsafe impl Sync for ObjectInfo {}
 
-impl<T: TypeMemory + TypeTrace + Clone> From<GcPtr> for *const ObjectInfo<T> {
+impl From<GcPtr> for *const ObjectInfo {
     fn from(ptr: GcPtr) -> Self {
         ptr.as_ptr() as Self
     }
 }
 
-impl<T: TypeMemory + TypeTrace + Clone> From<GcPtr> for *mut ObjectInfo<T> {
+impl From<GcPtr> for *mut ObjectInfo {
     fn from(ptr: GcPtr) -> Self {
         ptr.as_ptr() as Self
     }
 }
 
-impl<T: TypeMemory + TypeTrace + Clone> From<*const ObjectInfo<T>> for GcPtr {
-    fn from(info: *const ObjectInfo<T>) -> Self {
+impl From<*const ObjectInfo> for GcPtr {
+    fn from(info: *const ObjectInfo) -> Self {
         (info as RawGcPtr).into()
     }
 }
 
-impl<T: TypeMemory + TypeTrace + Clone> From<*mut ObjectInfo<T>> for GcPtr {
-    fn from(info: *mut ObjectInfo<T>) -> Self {
+impl From<*mut ObjectInfo> for GcPtr {
+    fn from(info: *mut ObjectInfo) -> Self {
         (info as RawGcPtr).into()
     }
 }
diff --git a/crates/mun_memory/src/gc/object.rs b/crates/mun_memory/src/gc/object.rs
new file mode 100644
index 000000000..e69de29bb
diff --git a/crates/mun_memory/src/gc/root_ptr.rs b/crates/mun_memory/src/gc/root_ptr.rs
index 8874e7652..07f8289a9 100644
--- a/crates/mun_memory/src/gc/root_ptr.rs
+++ b/crates/mun_memory/src/gc/root_ptr.rs
@@ -1,18 +1,13 @@
-use crate::{
-    gc::{GcPtr, GcRuntime, HasIndirectionPtr, TypeTrace},
-    TypeMemory,
-};
-use std::marker::PhantomData;
+use crate::gc::{GcPtr, GcRuntime, HasIndirectionPtr};
 use std::sync::{Arc, Weak};
 
 /// A `GcPtr` that automatically roots and unroots its internal `GcPtr`.
-pub struct GcRootPtr<T: TypeMemory + TypeTrace, G: GcRuntime<T>> {
+pub struct GcRootPtr<G: GcRuntime> {
     handle: GcPtr,
     runtime: Weak<G>,
-    ty: PhantomData<T>,
 }
 
-impl<T: TypeMemory + TypeTrace, G: GcRuntime<T>> Clone for GcRootPtr<T, G> {
+impl<G: GcRuntime> Clone for GcRootPtr<G> {
     fn clone(&self) -> Self {
         if let Some(runtime) = self.runtime.upgrade() {
             runtime.root(self.handle)
@@ -20,19 +15,17 @@ impl<T: TypeMemory + TypeTrace, G: GcRuntime<T>> Clone for GcRootPtr<T, G> {
         Self {
             handle: self.handle,
             runtime: self.runtime.clone(),
-            ty: PhantomData,
         }
     }
 }
 
-impl<T: TypeMemory + TypeTrace, G: GcRuntime<T>> GcRootPtr<T, G> {
+impl<G: GcRuntime> GcRootPtr<G> {
     /// Constructs a new GCRootHandle from a runtime and a handle
     pub fn new(runtime: &Arc<G>, handle: GcPtr) -> Self {
         runtime.root(handle);
         Self {
             handle,
             runtime: Arc::downgrade(runtime),
-            ty: PhantomData,
         }
     }
 
@@ -52,13 +45,13 @@ impl<T: TypeMemory + TypeTrace, G: GcRuntime<T>> GcRootPtr<T, G> {
     }
 }
 
-impl<T: TypeMemory + TypeTrace, G: GcRuntime<T>> From<GcRootPtr<T, G>> for GcPtr {
-    fn from(ptr: GcRootPtr<T, G>) -> Self {
+impl<G: GcRuntime> From<GcRootPtr<G>> for GcPtr {
+    fn from(ptr: GcRootPtr<G>) -> Self {
         ptr.handle
     }
 }
 
-impl<T: TypeMemory + TypeTrace, G: GcRuntime<T>> Drop for GcRootPtr<T, G> {
+impl<G: GcRuntime> Drop for GcRootPtr<G> {
     fn drop(&mut self) {
         if let Some(runtime) = self.runtime.upgrade() {
             runtime.unroot(self.handle)
@@ -66,7 +59,7 @@ impl<T: TypeMemory + TypeTrace, G: GcRuntime<T>> Drop for GcRootPtr<T, G> {
     }
 }
 
-impl<T: TypeMemory + TypeTrace, G: GcRuntime<T>> HasIndirectionPtr for GcRootPtr<T, G> {
+impl<G: GcRuntime> HasIndirectionPtr for GcRootPtr<G> {
     unsafe fn deref<R: Sized>(&self) -> *const R {
         self.handle.deref()
     }
diff --git a/crates/mun_memory/src/lib.rs b/crates/mun_memory/src/lib.rs
index f96361fe0..1ca56cafd 100644
--- a/crates/mun_memory/src/lib.rs
+++ b/crates/mun_memory/src/lib.rs
@@ -1,52 +1,28 @@
-use std::alloc::Layout;
+pub use type_info::{FieldInfo, HasStaticTypeInfo, StructInfo, TypeInfo, TypeInfoData};
 
 mod cast;
 pub mod diff;
 pub mod gc;
 pub mod mapping;
+mod type_info;
+pub mod type_table;
+use thiserror::Error;
 
 pub mod prelude {
     pub use crate::diff::{diff, Diff, FieldDiff, FieldEditKind};
     pub use crate::mapping::{Action, FieldMapping};
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
-pub enum TypeGroup {
-    Primitive,
-    Struct,
-}
-
-impl<'t> From<&'t abi::TypeInfoData> for TypeGroup {
-    fn from(data: &'t abi::TypeInfoData) -> Self {
-        match data {
-            abi::TypeInfoData::Primitive => TypeGroup::Primitive,
-            abi::TypeInfoData::Struct(_) => TypeGroup::Struct,
-        }
-    }
-}
-
-/// A trait used to obtain a type's description.
-pub trait TypeDesc: Send + Sync {
-    /// Returns the name of this type.
-    fn name(&self) -> &str;
-    /// Returns the `Guid` of this type.
-    fn guid(&self) -> &abi::Guid;
-    /// Returns the `TypeGroup` of this type.
-    fn group(&self) -> TypeGroup;
-}
-
-/// A trait used to obtain a type's memory description.
-pub trait TypeMemory: Send + Sync {
-    /// Returns the memory layout of this type.
-    fn layout(&self) -> Layout;
-    /// Returns whether the memory is stack-allocated.
-    fn is_stack_allocated(&self) -> bool;
+    pub use crate::type_info::{StructInfo, TypeInfo, TypeInfoData};
 }
 
 /// A trait used to obtain a type's fields.
-pub trait TypeFields<T>: Send + Sync {
+pub trait TypeFields: Send + Sync {
     /// Returns the type's fields.
-    fn fields(&self) -> Vec<(&str, T)>;
-    /// Returns the type's fields' offsets.
-    fn offsets(&self) -> &[u16];
+    fn fields(&self) -> &[FieldInfo];
+}
+
+/// An error that can occur when trying to convert from an abi type to an internal type.
+#[derive(Debug, Error)]
+pub enum TryFromAbiError {
+    #[error("unknown TypeId '{0}'")]
+    UnknownTypeId(abi::TypeId),
 }
diff --git a/crates/mun_memory/src/mapping.rs b/crates/mun_memory/src/mapping.rs
index 9215a7a01..250ee6d88 100644
--- a/crates/mun_memory/src/mapping.rs
+++ b/crates/mun_memory/src/mapping.rs
@@ -1,46 +1,49 @@
 use crate::{
     diff::{diff, Diff, FieldDiff, FieldEditKind},
     gc::GcPtr,
-    TypeDesc, TypeFields, TypeGroup, TypeMemory,
+    type_info::TypeInfo,
+    TypeFields,
 };
 use std::{
     collections::{HashMap, HashSet},
-    hash::Hash,
+    sync::Arc,
 };
 
-pub struct Mapping<T: Eq + Hash, U: TypeDesc + TypeMemory> {
-    pub deletions: HashSet<T>,
-    pub conversions: HashMap<T, Conversion<U>>,
-    pub identical: Vec<(T, T)>,
+pub struct Mapping {
+    pub deletions: HashSet<Arc<TypeInfo>>,
+    pub conversions: HashMap<Arc<TypeInfo>, Conversion>,
+    pub identical: Vec<(Arc<TypeInfo>, Arc<TypeInfo>)>,
 }
 
-pub struct Conversion<T: TypeDesc + TypeMemory> {
-    pub field_mapping: Vec<FieldMapping<T>>,
-    pub new_ty: T,
+pub struct Conversion {
+    pub field_mapping: Vec<FieldMapping>,
+    pub new_ty: Arc<TypeInfo>,
 }
 
 /// Description of the mapping of a single field. When stored together with the new index, this
 /// provides all information necessary for a mapping function.
-pub struct FieldMapping<T: TypeDesc + TypeMemory> {
-    pub new_ty: T,
+pub struct FieldMapping {
+    pub new_ty: Arc<TypeInfo>,
     pub new_offset: usize,
-    pub action: Action<T>,
+    pub action: Action,
 }
 
 /// The `Action` to take when mapping memory from A to B.
 #[derive(Eq, PartialEq)]
-pub enum Action<T: TypeDesc + TypeMemory> {
-    Cast { old_offset: usize, old_ty: T },
-    Copy { old_offset: usize },
+pub enum Action {
+    Cast {
+        old_offset: usize,
+        old_ty: Arc<TypeInfo>,
+    },
+    Copy {
+        old_offset: usize,
+    },
     Insert,
 }
 
-impl<T> Mapping<T, T>
-where
-    T: TypeDesc + TypeFields<T> + TypeMemory + Copy + Eq + Hash,
-{
+impl Mapping {
     ///
-    pub fn new(old: &[T], new: &[T]) -> Self {
+    pub fn new(old: &[Arc<TypeInfo>], new: &[Arc<TypeInfo>]) -> Self {
         let diff = diff(old, new);
 
         let mut conversions = HashMap::new();
@@ -52,27 +55,29 @@ where
         for diff in diff.iter() {
             match diff {
                 Diff::Delete { index } => {
-                    deletions.insert(unsafe { *old.get_unchecked(*index) });
+                    deletions.insert(unsafe { old.get_unchecked(*index).clone() });
                 }
                 Diff::Edit {
                     diff,
                     old_index,
                     new_index,
                 } => {
-                    let old_ty = unsafe { *old.get_unchecked(*old_index) };
-                    let new_ty = unsafe { *new.get_unchecked(*new_index) };
-                    conversions.insert(old_ty, unsafe { field_mapping(old_ty, new_ty, diff) });
+                    let old_ty = unsafe { old.get_unchecked(*old_index) };
+                    let new_ty = unsafe { new.get_unchecked(*new_index) };
+                    conversions.insert(old_ty.clone(), unsafe {
+                        field_mapping(old_ty, new_ty, diff)
+                    });
                 }
                 Diff::Insert { index } => {
-                    insertions.insert(unsafe { *new.get_unchecked(*index) });
+                    insertions.insert(unsafe { new.get_unchecked(*index).clone() });
                 }
                 Diff::Move {
                     old_index,
                     new_index,
                 } => identical.push(unsafe {
                     (
-                        *old.get_unchecked(*old_index),
-                        *new.get_unchecked(*new_index),
+                        old.get_unchecked(*old_index).clone(),
+                        new.get_unchecked(*new_index).clone(),
                     )
                 }),
             }
@@ -80,19 +85,19 @@ where
 
         // These candidates are used to collect a list of `new_index -> old_index` mappings for
         // identical types.
-        let mut new_candidates: HashSet<T> = new
+        let mut new_candidates: HashSet<_> = new
             .iter()
             // Filter non-struct types
-            .filter(|ty| ty.group() == TypeGroup::Struct)
+            .filter(|ty| ty.data.is_struct())
             // Filter inserted structs
             .filter(|ty| !insertions.contains(*ty))
             .cloned()
             .collect();
 
-        let mut old_candidates: HashSet<T> = old
+        let mut old_candidates: HashSet<_> = old
             .iter()
             // Filter non-struct types
-            .filter(|ty| ty.group() == TypeGroup::Struct)
+            .filter(|ty| ty.data.is_struct())
             // Filter deleted structs
             .filter(|ty| !deletions.contains(*ty))
             // Filter edited types
@@ -139,11 +144,11 @@ where
 /// # Safety
 ///
 /// Expects the `diff` to be based on `old_ty` and `new_ty`. If not, it causes undefined behavior.
-pub unsafe fn field_mapping<T: Clone + TypeDesc + TypeFields<T> + TypeMemory>(
-    old_ty: T,
-    new_ty: T,
+pub unsafe fn field_mapping(
+    old_ty: &Arc<TypeInfo>,
+    new_ty: &Arc<TypeInfo>,
     diff: &[FieldDiff],
-) -> Conversion<T> {
+) -> Conversion {
     let old_fields = old_ty.fields();
 
     let deletions: HashSet<usize> = diff
@@ -233,8 +238,6 @@ pub unsafe fn field_mapping<T: Clone + TypeDesc + TypeFields<T> + TypeMemory>(
     }
 
     let new_fields = new_ty.fields();
-    let old_offsets = old_ty.offsets();
-    let new_offsets = new_ty.offsets();
     Conversion {
         field_mapping: mapping
             .into_iter()
@@ -242,15 +245,18 @@ pub unsafe fn field_mapping<T: Clone + TypeDesc + TypeFields<T> + TypeMemory>(
             .map(|(new_index, desc)| {
                 let old_offset = desc
                     .old_index
-                    .map(|idx| usize::from(*old_offsets.get_unchecked(idx)));
+                    .map(|idx| usize::from(old_fields.get_unchecked(idx).offset));
 
                 FieldMapping {
-                    new_ty: new_fields.get_unchecked(new_index).1.clone(),
-                    new_offset: usize::from(*new_offsets.get_unchecked(new_index)),
+                    new_ty: new_fields.get_unchecked(new_index).type_info.clone(),
+                    new_offset: usize::from(new_fields.get_unchecked(new_index).offset),
                     action: match desc.action {
                         ActionDesc::Cast => Action::Cast {
                             old_offset: old_offset.unwrap(),
-                            old_ty: old_fields.get_unchecked(desc.old_index.unwrap()).1.clone(),
+                            old_ty: old_fields
+                                .get_unchecked(desc.old_index.unwrap())
+                                .type_info
+                                .clone(),
                         },
                         ActionDesc::Copy => Action::Copy {
                             old_offset: old_offset.unwrap(),
@@ -260,15 +266,15 @@ pub unsafe fn field_mapping<T: Clone + TypeDesc + TypeFields<T> + TypeMemory>(
                 }
             })
             .collect(),
-        new_ty,
+        new_ty: new_ty.clone(),
     }
 }
 
 /// A trait used to map allocated memory using type differences.
-pub trait MemoryMapper<T: Eq + Hash + TypeDesc + TypeMemory> {
+pub trait MemoryMapper {
     /// Maps its allocated memory using the provided `mapping`.
     ///
     /// A `Vec<GcPtr>` is returned containing all objects of types that were deleted. The
     /// corresponding types have to remain in-memory until the objects have been deallocated.
-    fn map_memory(&self, mapping: Mapping<T, T>) -> Vec<GcPtr>;
+    fn map_memory(&self, mapping: Mapping) -> Vec<GcPtr>;
 }
diff --git a/crates/mun_memory/src/type_info.rs b/crates/mun_memory/src/type_info.rs
new file mode 100644
index 000000000..a70aa5e56
--- /dev/null
+++ b/crates/mun_memory/src/type_info.rs
@@ -0,0 +1,320 @@
+use crate::{type_table::TypeTable, TryFromAbiError, TypeFields};
+use abi::static_type_map::StaticTypeMap;
+use abi::{Guid, StructMemoryKind};
+use itertools::izip;
+use std::sync::Once;
+use std::{
+    alloc::Layout,
+    fmt::{self, Formatter},
+    sync::Arc,
+};
+
+/// A linked version of [`mun_abi::TypeInfo`] that has resolved all occurrences of `TypeId` with `TypeInfo`.
+#[derive(Clone, Debug)]
+pub struct TypeInfo {
+    /// Type ID
+    pub id: abi::TypeId,
+    /// Type name
+    pub name: String,
+    // TODO: Move layout to TypeInfoData
+    /// The memory layout of the type
+    pub layout: Layout,
+    /// Type group
+    pub data: TypeInfoData,
+}
+
+/// A linked version of [`mun_abi::TypeInfoData`] that has resolved all occurrences of `TypeId` with `TypeInfo`.
+#[repr(u8)]
+#[derive(Clone, Debug)]
+pub enum TypeInfoData {
+    /// Primitive types (i.e. `()`, `bool`, `float`, `int`, etc.)
+    Primitive,
+    /// Struct types (i.e. record, tuple, or unit structs)
+    Struct(StructInfo),
+}
+
+/// A linked version of [`mun_abi::StructInfo`] that has resolved all occurrences of `TypeId` with `TypeInfo`.  
+#[derive(Clone, Debug)]
+pub struct StructInfo {
+    /// Struct fields
+    pub fields: Vec<FieldInfo>,
+    /// Struct memory kind
+    pub memory_kind: abi::StructMemoryKind,
+}
+
+impl fmt::Display for TypeInfo {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.name)
+    }
+}
+
+impl PartialEq for TypeInfo {
+    fn eq(&self, other: &Self) -> bool {
+        self.id == other.id
+    }
+}
+
+impl Eq for TypeInfo {}
+
+impl std::hash::Hash for TypeInfo {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.id.hash(state);
+    }
+}
+
+impl TypeFields for Arc<TypeInfo> {
+    fn fields(&self) -> &[FieldInfo] {
+        if let TypeInfoData::Struct(s) = &self.data {
+            &s.fields
+        } else {
+            &[]
+        }
+    }
+}
+
+impl TypeInfo {
+    /// Returns true if this instance represents the TypeInfo of the given type.
+    ///
+    /// ```rust
+    /// # use mun_memory::HasStaticTypeInfo;
+    /// assert!(i64::type_info().equals::<i64>());
+    /// assert!(!i64::type_info().equals::<f64>())
+    /// ```
+    pub fn equals<T: HasStaticTypeInfo>(&self) -> bool {
+        T::type_info().as_ref() == self
+    }
+
+    /// Returns whether this is a fundamental type.
+    pub fn is_primitive(&self) -> bool {
+        self.data.is_primitive()
+    }
+
+    /// Returns whether this is a struct type.
+    pub fn is_struct(&self) -> bool {
+        self.data.is_struct()
+    }
+
+    /// Retrieves the type's struct information, if available.
+    pub fn as_struct(&self) -> Option<&StructInfo> {
+        if let TypeInfoData::Struct(s) = &self.data {
+            Some(s)
+        } else {
+            None
+        }
+    }
+
+    /// Returns whether the type is allocated on the stack.
+    pub fn is_stack_allocated(&self) -> bool {
+        match &self.data {
+            TypeInfoData::Primitive => true,
+            TypeInfoData::Struct(s) => s.memory_kind == StructMemoryKind::Value,
+        }
+    }
+
+    /// Tries to convert from an `abi::TypeInfo`.
+    pub fn try_from_abi(
+        type_info: &abi::TypeInfo,
+        type_table: &TypeTable,
+    ) -> Result<TypeInfo, TryFromAbiError> {
+        TypeInfoData::try_from_abi(&type_info.data, type_table).map(|data| TypeInfo {
+            id: type_info.id.clone(),
+            name: type_info.name().to_owned(),
+            layout: Layout::from_size_align(type_info.size_in_bytes(), type_info.alignment())
+                .unwrap_or_else(|_| {
+                    panic!(
+                        "TypeInfo contains invalid size and alignment (size: {}, align: {})",
+                        type_info.size_in_bytes(),
+                        type_info.alignment()
+                    )
+                }),
+            data,
+        })
+    }
+}
+
+impl TypeInfoData {
+    /// Returns whether this is a fundamental type.
+    pub fn is_primitive(&self) -> bool {
+        matches!(self, TypeInfoData::Primitive)
+    }
+
+    /// Returns whether this is a struct type.
+    pub fn is_struct(&self) -> bool {
+        matches!(self, TypeInfoData::Struct(_))
+    }
+
+    /// Tries to convert from an `abi::TypeInfoData`.
+    pub fn try_from_abi(
+        type_info_data: &abi::TypeInfoData,
+        type_table: &TypeTable,
+    ) -> Result<TypeInfoData, TryFromAbiError> {
+        match type_info_data {
+            abi::TypeInfoData::Primitive => Ok(TypeInfoData::Primitive),
+            abi::TypeInfoData::Struct(s) => {
+                StructInfo::try_from_abi(s, type_table).map(TypeInfoData::Struct)
+            }
+        }
+    }
+}
+
+impl StructInfo {
+    /// Returns the `TypeInfo` and offset corresponding to the field matching the specified `field_name`, if it exists.
+    pub fn find_field_by_name<S: AsRef<str>>(&self, field_name: S) -> Option<&FieldInfo> {
+        self.fields
+            .iter()
+            .find(|field| field.name == field_name.as_ref())
+    }
+
+    /// Tries to convert from an `abi::StructInfo`.
+    pub fn try_from_abi(
+        struct_info: &abi::StructInfo,
+        type_table: &TypeTable,
+    ) -> Result<StructInfo, TryFromAbiError> {
+        let fields: Result<Vec<FieldInfo>, TryFromAbiError> = izip!(
+            struct_info.field_names(),
+            struct_info.field_types(),
+            struct_info.field_offsets()
+        )
+        .map(|(name, type_id, offset)| {
+            type_table
+                .find_type_info_by_id(type_id)
+                .ok_or_else(|| TryFromAbiError::UnknownTypeId(type_id.clone()))
+                .map(|type_info| FieldInfo {
+                    name: name.to_owned(),
+                    type_info,
+                    offset: *offset,
+                })
+        })
+        .collect();
+
+        fields.map(|fields| StructInfo {
+            fields,
+            memory_kind: struct_info.memory_kind,
+        })
+    }
+}
+
+/// A linked version of a struct field.
+#[derive(Clone, Debug)]
+pub struct FieldInfo {
+    /// The field's name
+    pub name: String,
+    /// The field's type
+    pub type_info: Arc<TypeInfo>,
+    /// The field's offset
+    pub offset: u16,
+    // TODO: Field accessibility levels
+    // const MunPrivacy_t *field_privacies,
+}
+
+/// A trait that defines static type information for types that can provide it.
+pub trait HasStaticTypeInfo {
+    fn type_info() -> &'static Arc<TypeInfo>;
+}
+
+macro_rules! impl_primitive_type {
+    ($($ty:ty),+) => {
+        $(
+            impl HasStaticTypeInfo for $ty {
+                fn type_info() -> &'static Arc<TypeInfo> {
+                    static TYPE_INFO: once_cell::sync::OnceCell<Arc<TypeInfo>> = once_cell::sync::OnceCell::new();
+                    TYPE_INFO.get_or_init(|| {
+                        let type_info = <$ty as abi::HasStaticTypeInfo>::type_info();
+                        Arc::new(TypeInfo {
+                            id: type_info.id.clone(),
+                            name: type_info.name().to_owned(),
+                            layout:  Layout::from_size_align(type_info.size_in_bytes(), type_info.alignment())
+                                .expect("TypeInfo contains invalid size and alignment."),
+                            data: TypeInfoData::Primitive
+                        })
+                    })
+                }
+            }
+        )+
+    }
+}
+
+impl_primitive_type!(
+    i8,
+    i16,
+    i32,
+    i64,
+    i128,
+    isize,
+    u8,
+    u16,
+    u32,
+    u64,
+    u128,
+    usize,
+    f32,
+    f64,
+    bool,
+    ()
+);
+
+/// Every type that has at least a type name also has a valid pointer type name
+impl<T: abi::HasStaticTypeInfoName + 'static> HasStaticTypeInfo for *mut T {
+    fn type_info() -> &'static Arc<TypeInfo> {
+        static mut VALUE: Option<StaticTypeMap<Arc<TypeInfo>>> = None;
+        static INIT: Once = Once::new();
+
+        let map = unsafe {
+            INIT.call_once(|| {
+                VALUE = Some(StaticTypeMap::default());
+            });
+            VALUE.as_ref().unwrap()
+        };
+
+        map.call_once::<T, _>(|| {
+            let name = format!(
+                "*mut {}",
+                <T as abi::HasStaticTypeInfoName>::type_name()
+                    .to_str()
+                    .expect("static type name is not a valid UTF-8 string")
+            );
+            let size_in_bits = std::mem::size_of::<*mut T>();
+            let alignment = std::mem::align_of::<*mut T>();
+            Arc::new(TypeInfo {
+                id: Guid::from(name.as_bytes()).into(),
+                name,
+                layout: Layout::from_size_align(size_in_bits, alignment)
+                    .expect("invalid layout for static type"),
+                data: TypeInfoData::Primitive,
+            })
+        })
+    }
+}
+
+/// Every type that has at least a type name also has a valid pointer type name
+impl<T: abi::HasStaticTypeInfoName + 'static> HasStaticTypeInfo for *const T {
+    fn type_info() -> &'static Arc<TypeInfo> {
+        static mut VALUE: Option<StaticTypeMap<Arc<TypeInfo>>> = None;
+        static INIT: Once = Once::new();
+
+        let map = unsafe {
+            INIT.call_once(|| {
+                VALUE = Some(StaticTypeMap::default());
+            });
+            VALUE.as_ref().unwrap()
+        };
+
+        map.call_once::<T, _>(|| {
+            let name = format!(
+                "*const {}",
+                <T as abi::HasStaticTypeInfoName>::type_name()
+                    .to_str()
+                    .expect("static type name is not a valid UTF-8 string")
+            );
+            let size_in_bits = std::mem::size_of::<*const T>();
+            let alignment = std::mem::align_of::<*const T>();
+            Arc::new(TypeInfo {
+                id: Guid::from(name.as_bytes()).into(),
+                name,
+                layout: Layout::from_size_align(size_in_bits, alignment)
+                    .expect("invalid layout for static type"),
+                data: TypeInfoData::Primitive,
+            })
+        })
+    }
+}
diff --git a/crates/mun_memory/src/type_table.rs b/crates/mun_memory/src/type_table.rs
new file mode 100644
index 000000000..66d23ac90
--- /dev/null
+++ b/crates/mun_memory/src/type_table.rs
@@ -0,0 +1,87 @@
+use crate::type_info::{HasStaticTypeInfo, TypeInfo};
+use rustc_hash::FxHashMap;
+use std::sync::Arc;
+
+#[derive(Clone)]
+pub struct TypeTable {
+    type_id_to_type_info: FxHashMap<abi::TypeId, Arc<TypeInfo>>,
+    type_name_to_type_info: FxHashMap<String, Arc<TypeInfo>>,
+}
+
+impl TypeTable {
+    pub fn find_type_info_by_name<S: AsRef<str>>(&self, name: S) -> Option<Arc<TypeInfo>> {
+        self.type_name_to_type_info
+            .get(name.as_ref())
+            .map(Clone::clone)
+    }
+
+    pub fn find_type_info_by_id(&self, type_id: &abi::TypeId) -> Option<Arc<TypeInfo>> {
+        self.type_id_to_type_info.get(type_id).map(Clone::clone)
+    }
+
+    /// Inserts `type_info` into the type table.
+    ///
+    /// If the type table already contained this `type_info`, the value is updated, and the old
+    /// value is returned.
+    pub fn insert_type(&mut self, type_info: Arc<TypeInfo>) -> Option<Arc<TypeInfo>> {
+        self.type_id_to_type_info
+            .insert(type_info.id.clone(), type_info.clone());
+        self.type_name_to_type_info
+            .insert(type_info.name.clone(), type_info)
+    }
+
+    /// Removes and returns the `TypeInfo` corresponding to `type_info`, if it exists.
+    pub fn remove_type(&mut self, type_info: &TypeInfo) -> Option<Arc<TypeInfo>> {
+        self.type_id_to_type_info.remove(&type_info.id);
+        self.type_name_to_type_info.remove(&type_info.name)
+    }
+
+    /// Removes and returns the `TypeInfo` corresponding to `type_id`, if it exists.
+    pub fn remove_type_by_id(&mut self, type_id: &abi::TypeId) -> Option<Arc<TypeInfo>> {
+        let type_info = self.type_id_to_type_info.remove(type_id);
+        if let Some(type_info) = &type_info {
+            self.type_name_to_type_info.remove(&type_info.name);
+        }
+        type_info
+    }
+
+    /// Removes and returns the `TypeInfo` corresponding to `name`, if it exists.
+    pub fn remove_type_by_name<S: AsRef<str>>(&mut self, name: S) -> Option<Arc<TypeInfo>> {
+        let type_info = self.type_name_to_type_info.remove(name.as_ref());
+        if let Some(type_info) = &type_info {
+            self.type_id_to_type_info.remove(&type_info.id);
+        }
+        type_info
+    }
+}
+
+impl Default for TypeTable {
+    fn default() -> Self {
+        let mut type_table = Self {
+            type_id_to_type_info: Default::default(),
+            type_name_to_type_info: Default::default(),
+        };
+
+        type_table.insert_type(i8::type_info().clone());
+        type_table.insert_type(i16::type_info().clone());
+        type_table.insert_type(i32::type_info().clone());
+        type_table.insert_type(i64::type_info().clone());
+        type_table.insert_type(i128::type_info().clone());
+        type_table.insert_type(u8::type_info().clone());
+        type_table.insert_type(u16::type_info().clone());
+        type_table.insert_type(u32::type_info().clone());
+        type_table.insert_type(u64::type_info().clone());
+        type_table.insert_type(u128::type_info().clone());
+        type_table.insert_type(f32::type_info().clone());
+        type_table.insert_type(f64::type_info().clone());
+        type_table.insert_type(bool::type_info().clone());
+        type_table.insert_type(<()>::type_info().clone());
+
+        // Types used by the FFI interface
+        type_table.insert_type(<*const std::ffi::c_void>::type_info().clone());
+        type_table.insert_type(<*mut std::ffi::c_void>::type_info().clone());
+        type_table.insert_type(<*const *mut std::ffi::c_void>::type_info().clone());
+
+        type_table
+    }
+}
diff --git a/crates/mun_runtime/src/adt.rs b/crates/mun_runtime/src/adt.rs
index 00bdcdc45..3be105f2b 100644
--- a/crates/mun_runtime/src/adt.rs
+++ b/crates/mun_runtime/src/adt.rs
@@ -1,12 +1,13 @@
-use crate::garbage_collector::{GcPtr, GcRootPtr, UnsafeTypeInfo};
 use crate::{
+    garbage_collector::GcRootPtr,
     marshal::Marshal,
-    reflection::{
-        equals_argument_type, equals_return_type, ArgumentReflection, ReturnTypeReflection,
-    },
+    reflection::{equals_return_type, ArgumentReflection, ReturnTypeReflection},
     GarbageCollector, Runtime,
 };
-use memory::gc::{GcRuntime, HasIndirectionPtr};
+use memory::{
+    gc::{GcPtr, GcRuntime, HasIndirectionPtr},
+    TypeInfo,
+};
 use once_cell::sync::OnceCell;
 use std::{
     ptr::{self, NonNull},
@@ -53,26 +54,20 @@ impl<'s> StructRef<'s> {
     }
 
     /// Returns the type information of the struct.
-    pub fn type_info(&self) -> &abi::TypeInfo {
-        // Safety: The type returned from `ptr_type` is guaranteed to live at least as long as
-        // `Runtime` does not change. As the lifetime of `TypeInfo` is tied to the lifetime of
-        // `Runtime`, this is safe.
-        unsafe { &*self.runtime.gc.ptr_type(self.raw.0).into_inner().as_ptr() }
+    pub fn type_info(&self) -> Arc<TypeInfo> {
+        self.runtime.gc.ptr_type(self.raw.0)
     }
 
-    ///
+    /// Returns the struct's field at the specified `offset`.
     ///
     /// # Safety
     ///
-    ///
-    unsafe fn field_offset_unchecked<T>(
-        &self,
-        struct_info: &abi::StructInfo,
-        field_idx: usize,
-    ) -> NonNull<T> {
-        let offset = *struct_info.field_offsets().get_unchecked(field_idx);
-        // Safety: self.raw's memory pointer is never null
-        NonNull::new_unchecked(self.raw.get_ptr().add(offset as usize).cast::<T>() as *mut _)
+    /// The offset must be the location of a variable of type T.
+    unsafe fn get_field_ptr_unchecked<T>(&self, offset: usize) -> NonNull<T> {
+        // SAFETY: self.raw's memory pointer is never null
+        let ptr = self.raw.get_ptr();
+
+        NonNull::new_unchecked(ptr.add(offset).cast::<T>() as *mut T)
     }
 
     /// Retrieves the value of the field corresponding to the specified `field_name`.
@@ -84,29 +79,28 @@ impl<'s> StructRef<'s> {
 
         // Safety: `as_struct` is guaranteed to return `Some` for `StructRef`s.
         let struct_info = type_info.as_struct().unwrap();
-        let field_idx =
-            abi::StructInfo::find_field_index(type_info.name(), struct_info, field_name)?;
 
-        // Safety: If we found the `field_idx`, we are guaranteed to also have the `field_type` and
-        // `field_offset`.
-        let field_type = unsafe { struct_info.field_types().get_unchecked(field_idx) };
-        equals_return_type::<T>(field_type).map_err(|(expected, found)| {
+        let field_info = struct_info.find_field_by_name(field_name).ok_or_else(|| {
+            format!(
+                "Struct `{}` does not contain field `{}`.",
+                type_info.name, field_name
+            )
+        })?;
+
+        equals_return_type::<T>(&field_info.type_info).map_err(|(expected, found)| {
             format!(
                 "Mismatched types for `{}::{}`. Expected: `{}`. Found: `{}`.",
-                type_info.name(),
-                field_name,
-                expected,
-                found,
+                type_info.name, field_name, expected, found,
             )
         })?;
 
-        // If we found the `field_idx`, we are guaranteed to also have the `field_offset`
+        // SAFETY: The offset in the ABI is always valid.
         let field_ptr =
-            unsafe { self.field_offset_unchecked::<T::MunType>(struct_info, field_idx) };
+            unsafe { self.get_field_ptr_unchecked::<T::MunType>(usize::from(field_info.offset)) };
         Ok(Marshal::marshal_from_ptr(
             field_ptr,
             self.runtime,
-            Some(field_type),
+            &field_info.type_info,
         ))
     }
 
@@ -124,26 +118,29 @@ impl<'s> StructRef<'s> {
 
         // Safety: `as_struct` is guaranteed to return `Some` for `StructRef`s.
         let struct_info = type_info.as_struct().unwrap();
-        let field_idx =
-            abi::StructInfo::find_field_index(type_info.name(), struct_info, field_name)?;
 
-        // Safety: If we found the `field_idx`, we are guaranteed to also have the `field_type` and
-        // `field_offset`.
-        let field_type = unsafe { struct_info.field_types().get_unchecked(field_idx) };
-        equals_argument_type(self.runtime, field_type, &value).map_err(|(expected, found)| {
+        let field_info = struct_info.find_field_by_name(field_name).ok_or_else(|| {
             format!(
-                "Mismatched types for `{}::{}`. Expected: `{}`. Found: `{}`.",
-                type_info.name(),
-                field_name,
-                expected,
-                found,
+                "Struct `{}` does not contain field `{}`.",
+                type_info.name, field_name
             )
         })?;
 
+        if field_info.type_info.id != value.type_id(self.runtime) {
+            return Err(format!(
+                "Mismatched types for `{}::{}`. Expected: `{}`. Found: `{}`.",
+                type_info.name,
+                field_name,
+                value.type_info(self.runtime).name,
+                field_info.type_info
+            ));
+        }
+
+        // SAFETY: The offset in the ABI is always valid.
         let field_ptr =
-            unsafe { self.field_offset_unchecked::<T::MunType>(struct_info, field_idx) };
-        let old = Marshal::marshal_from_ptr(field_ptr, self.runtime, Some(field_type));
-        Marshal::marshal_to_ptr(value, field_ptr, Some(field_type));
+            unsafe { self.get_field_ptr_unchecked::<T::MunType>(usize::from(field_info.offset)) };
+        let old = Marshal::marshal_from_ptr(field_ptr, self.runtime, &field_info.type_info);
+        Marshal::marshal_to_ptr(value, field_ptr, &field_info.type_info);
         Ok(old)
     }
 
@@ -157,52 +154,35 @@ impl<'s> StructRef<'s> {
 
         // Safety: `as_struct` is guaranteed to return `Some` for `StructRef`s.
         let struct_info = type_info.as_struct().unwrap();
-        let field_idx =
-            abi::StructInfo::find_field_index(type_info.name(), struct_info, field_name)?;
 
-        // Safety: If we found the `field_idx`, we are guaranteed to also have the `field_type` and
-        // `field_offset`.
-        let field_type = unsafe { struct_info.field_types().get_unchecked(field_idx) };
-        equals_argument_type(self.runtime, field_type, &value).map_err(|(expected, found)| {
+        let field_info = struct_info.find_field_by_name(field_name).ok_or_else(|| {
             format!(
-                "Mismatched types for `{}::{}`. Expected: `{}`. Found: `{}`.",
-                type_info.name(),
-                field_name,
-                expected,
-                found,
+                "Struct `{}` does not contain field `{}`.",
+                type_info.name, field_name
             )
         })?;
 
+        if field_info.type_info.id != value.type_id(self.runtime) {
+            return Err(format!(
+                "Mismatched types for `{}::{}`. Expected: `{}`. Found: `{}`.",
+                type_info.name,
+                field_name,
+                value.type_info(self.runtime).name,
+                field_info.type_info
+            ));
+        }
+
+        // SAFETY: The offset in the ABI is always valid.
         let field_ptr =
-            unsafe { self.field_offset_unchecked::<T::MunType>(struct_info, field_idx) };
-        Marshal::marshal_to_ptr(value, field_ptr, Some(field_type));
+            unsafe { self.get_field_ptr_unchecked::<T::MunType>(usize::from(field_info.offset)) };
+        Marshal::marshal_to_ptr(value, field_ptr, &field_info.type_info);
         Ok(())
     }
 }
 
 impl<'r> ArgumentReflection for StructRef<'r> {
-    fn type_guid(&self, runtime: &Runtime) -> abi::Guid {
-        // Safety: The type returned from `ptr_type` is guaranteed to live at least as long as
-        // `Runtime` does not change. As we hold a shared reference to `Runtime`, this is safe.
-        unsafe { runtime.gc().ptr_type(self.raw.0).into_inner().as_ref().guid }
-    }
-
-    fn type_name(&self, runtime: &Runtime) -> &str {
-        // Safety: The type returned from `ptr_type` is guaranteed to live at least as long as
-        // `Runtime` does not change. As we hold a shared reference to `Runtime`, this is safe.
-        unsafe { (&*runtime.gc().ptr_type(self.raw.0).into_inner().as_ptr()).name() }
-    }
-}
-
-impl<'r> ReturnTypeReflection for StructRef<'r> {
-    fn type_name() -> &'static str {
-        "struct"
-    }
-
-    fn type_guid() -> abi::Guid {
-        // TODO: Once `const_fn` lands, replace this with a const md5 hash
-        static GUID: OnceCell<abi::Guid> = OnceCell::new();
-        *GUID.get_or_init(|| abi::Guid(md5::compute(<Self as ReturnTypeReflection>::type_name()).0))
+    fn type_info(&self, _runtime: &Runtime) -> Arc<TypeInfo> {
+        self.type_info()
     }
 }
 
@@ -223,14 +203,12 @@ impl<'s> Marshal<'s> for StructRef<'s> {
     fn marshal_from_ptr<'r>(
         ptr: NonNull<Self::MunType>,
         runtime: &'r Runtime,
-        type_info: Option<&abi::TypeInfo>,
+        type_info: &Arc<TypeInfo>,
     ) -> StructRef<'s>
     where
         Self: 's,
         'r: 's,
     {
-        // Safety: `type_info` is only `None` for the `()` type
-        let type_info = type_info.unwrap();
         let struct_info = type_info.as_struct().unwrap();
 
         // Copy the contents of the struct based on what kind of pointer we are dealing with
@@ -238,20 +216,12 @@ impl<'s> Marshal<'s> for StructRef<'s> {
             // For a value struct, `ptr` points to a struct value.
 
             // Create a new object using the runtime's intrinsic
-            let mut gc_handle = {
-                runtime.gc().alloc(
-                    // Safety: `ty` is a shared reference, so is guaranteed to not be `ptr::null()`.
-                    UnsafeTypeInfo::new(unsafe {
-                        NonNull::new_unchecked(type_info as *const abi::TypeInfo as *mut _)
-                    }),
-                )
-            };
+            let mut gc_handle = runtime.gc().alloc(type_info);
 
             // Construct
             let src = ptr.cast::<u8>().as_ptr() as *const _;
             let dest = unsafe { gc_handle.deref_mut::<u8>() };
-            let size = type_info.size_in_bytes();
-            unsafe { ptr::copy_nonoverlapping(src, dest, size) };
+            unsafe { ptr::copy_nonoverlapping(src, dest, type_info.layout.size()) };
 
             gc_handle
         } else {
@@ -262,25 +232,34 @@ impl<'s> Marshal<'s> for StructRef<'s> {
         StructRef::new(RawStruct(gc_handle), runtime)
     }
 
-    fn marshal_to_ptr(
-        value: Self,
-        mut ptr: NonNull<Self::MunType>,
-        type_info: Option<&abi::TypeInfo>,
-    ) {
-        // `type_info` is only `None` for the `()` type
-        let type_info = type_info.unwrap();
-
+    fn marshal_to_ptr(value: Self, mut ptr: NonNull<Self::MunType>, type_info: &Arc<TypeInfo>) {
         let struct_info = type_info.as_struct().unwrap();
         if struct_info.memory_kind == abi::StructMemoryKind::Value {
             let dest = ptr.cast::<u8>().as_ptr();
-            let size = type_info.size_in_bytes();
-            unsafe { ptr::copy_nonoverlapping(value.into_raw().get_ptr(), dest, size as usize) };
+            unsafe {
+                ptr::copy_nonoverlapping(value.into_raw().get_ptr(), dest, type_info.layout.size())
+            };
         } else {
             unsafe { *ptr.as_mut() = value.into_raw() };
         }
     }
 }
 
+impl<'r> ReturnTypeReflection for StructRef<'r> {
+    fn type_name() -> &'static str {
+        "struct"
+    }
+
+    fn type_id() -> abi::TypeId {
+        // TODO: Once `const_fn` lands, replace this with a const md5 hash
+        static GUID: OnceCell<abi::TypeId> = OnceCell::new();
+        GUID.get_or_init(|| {
+            abi::Guid::from(<Self as ReturnTypeReflection>::type_name().as_bytes()).into()
+        })
+        .clone()
+    }
+}
+
 /// Type-agnostic wrapper for interoperability with a Mun struct, that has been rooted. To marshal,
 /// obtain a `StructRef` for the `RootedStruct`.
 #[derive(Clone)]
@@ -291,9 +270,7 @@ pub struct RootedStruct {
 impl RootedStruct {
     /// Creates a `RootedStruct` that wraps a raw Mun struct.
     fn new(gc: &Arc<GarbageCollector>, raw: RawStruct) -> Self {
-        // Safety: The type returned from `ptr_type` is guaranteed to live at least as long as
-        // `Runtime` does not change. As we hold a shared reference to `Runtime`, this is safe.
-        assert!(unsafe { gc.ptr_type(raw.0).into_inner().as_ref().data.is_struct() });
+        assert!(gc.ptr_type(raw.0).is_struct());
         Self {
             handle: GcRootPtr::new(gc, raw.0),
         }
diff --git a/crates/mun_runtime/src/assembly.rs b/crates/mun_runtime/src/assembly.rs
index 5e17f2f99..900381a53 100644
--- a/crates/mun_runtime/src/assembly.rs
+++ b/crates/mun_runtime/src/assembly.rs
@@ -1,17 +1,16 @@
-use crate::{
-    garbage_collector::{GarbageCollector, UnsafeTypeInfo},
-    DispatchTable,
-};
-use abi::{AssemblyInfo, FunctionPrototype};
+use crate::{garbage_collector::GarbageCollector, DispatchTable};
 use anyhow::anyhow;
 use libloader::{MunLibrary, TempLibrary};
 use log::error;
-use memory::mapping::{Mapping, MemoryMapper};
+use memory::{
+    mapping::{Mapping, MemoryMapper},
+    type_table::TypeTable,
+    TryFromAbiError, TypeInfo,
+};
 use std::{
-    collections::HashMap,
+    collections::{HashMap, VecDeque},
     ffi::c_void,
     path::{Path, PathBuf},
-    ptr::NonNull,
     sync::Arc,
 };
 
@@ -19,8 +18,7 @@ use std::{
 pub struct Assembly {
     library_path: PathBuf,
     library: TempLibrary,
-    legacy_libs: Vec<TempLibrary>,
-    info: AssemblyInfo,
+    info: abi::AssemblyInfo,
     allocator: Arc<GarbageCollector>,
 }
 
@@ -62,7 +60,6 @@ impl Assembly {
         let assembly = Assembly {
             library_path: library_path.to_path_buf(),
             library: library.into_inner(),
-            legacy_libs: Vec::new(),
             info,
             allocator: gc,
         };
@@ -70,10 +67,35 @@ impl Assembly {
         Ok(assembly)
     }
 
+    fn link_all_types<'a>(
+        type_table: &mut TypeTable,
+        to_link: impl Iterator<Item = (&'a abi::TypeId, &'a mut *const c_void, &'a str)>,
+    ) -> anyhow::Result<()> {
+        // Try to link all LUT entries
+        let mut failed_to_link = false;
+        for (type_id, type_info_ptr, debug_name) in to_link {
+            // Ensure that the function is in the runtime dispatch table
+            if let Some(type_info) = type_table.find_type_info_by_id(type_id) {
+                *type_info_ptr = Arc::into_raw(type_info) as *const c_void;
+            } else {
+                dbg!(debug_name);
+                failed_to_link = true;
+            }
+        }
+
+        if failed_to_link {
+            return Err(anyhow!(
+                "Failed to link types due to missing type dependencies."
+            ));
+        }
+
+        Ok(())
+    }
+
     /// Private implementation of runtime linking
-    fn link_all_impl<'a>(
+    fn link_all_functions<'a>(
         dispatch_table: &mut DispatchTable,
-        to_link: impl Iterator<Item = (&'a mut *const c_void, &'a FunctionPrototype)>,
+        to_link: impl Iterator<Item = (&'a mut *const c_void, &'a abi::FunctionPrototype)>,
     ) -> anyhow::Result<()> {
         let mut to_link: Vec<_> = to_link.collect();
 
@@ -86,9 +108,17 @@ impl Assembly {
             for (dispatch_ptr, fn_prototype) in to_link.into_iter() {
                 // Ensure that the function is in the runtime dispatch table
                 if let Some(fn_def) = dispatch_table.get_fn(fn_prototype.name()) {
+                    let type_ids = fn_def
+                        .prototype
+                        .signature
+                        .arg_types
+                        .iter()
+                        .map(|type_info| &type_info.id);
+
                     // Ensure that the function's signature is the same.
-                    if fn_prototype.signature != fn_def.prototype.signature {
-                        return Err(anyhow!("Failed to link: function '{}' is missing. A function with the same name does exist, but the signatures do not match (expected: {}, found: {}).", fn_prototype.name(), fn_prototype, fn_def.prototype));
+                    if !fn_prototype.signature.arg_types().iter().eq(type_ids) {
+                        return Err(anyhow!("Failed to link: function '{}' is missing. A function with the same name does exist, but the signatures do not match.", fn_prototype.name()));
+                        // (expected: {}, found: {}).", fn_prototype.name(), fn_prototype, fn_def.prototype));
                     }
 
                     *dispatch_ptr = fn_def.fn_ptr;
@@ -121,149 +151,208 @@ impl Assembly {
     pub(super) fn link_all<'a>(
         assemblies: impl Iterator<Item = &'a mut Assembly>,
         dispatch_table: &DispatchTable,
-    ) -> anyhow::Result<DispatchTable> {
-        let assemblies: Vec<&'a mut _> = assemblies.collect();
+        type_table: &TypeTable,
+    ) -> anyhow::Result<(DispatchTable, TypeTable)> {
+        let mut assemblies: Vec<&'a mut _> = assemblies.collect();
+
+        // Clone the type table, such that we can roll back if linking fails
+        let mut type_table = type_table.clone();
+
+        // Collect all types that need to be loaded
+        let mut types_to_load: VecDeque<&abi::TypeInfo> = assemblies
+            .iter()
+            .flat_map(|asm| asm.info().symbols.types().iter())
+            .collect();
+
+        // Load all types
+        while let Some(type_info) = types_to_load.pop_front() {
+            match TypeInfo::try_from_abi(type_info, &type_table) {
+                Ok(type_info) => {
+                    assert!(type_table.insert_type(Arc::new(type_info)).is_none())
+                }
+                Err(TryFromAbiError::UnknownTypeId(_)) => types_to_load.push_back(type_info),
+            }
+        }
+
+        let types_to_link = assemblies
+            .iter_mut()
+            .flat_map(|asm| asm.info.type_lut.iter_mut())
+            // Only take signatures into account that do *not* yet have a type handle assigned
+            // by the compiler.
+            .filter(|(_, ptr, _)| ptr.is_null());
+
+        Assembly::link_all_types(&mut type_table, types_to_link)?;
 
         // Clone the dispatch table, such that we can roll back if linking fails
         let mut dispatch_table = dispatch_table.clone();
 
         // Insert all assemblies' functions into the dispatch table
         for assembly in assemblies.iter() {
-            for function in assembly.info().symbols.functions() {
-                dispatch_table.insert_fn(function.prototype.name(), function.clone());
-            }
+            dispatch_table.insert_module(&assembly.info().symbols, &type_table);
         }
 
-        let to_link = assemblies
+        let functions_to_link = assemblies
             .into_iter()
             .flat_map(|asm| asm.info.dispatch_table.iter_mut())
             // Only take signatures into account that do *not* yet have a function pointer assigned
             // by the compiler.
             .filter(|(ptr, _)| ptr.is_null());
 
-        Assembly::link_all_impl(&mut dispatch_table, to_link)?;
+        Assembly::link_all_functions(&mut dispatch_table, functions_to_link)?;
 
-        Ok(dispatch_table)
+        Ok((dispatch_table, type_table))
     }
 
-    /// Tries to link the `assemblies`, resulting in a new [`DispatchTable`] on success. This leaves
-    /// the original `dispatch_table` intact, in case of linking errors.
+    /// Tries to link the `unlinked_assemblies`, resulting in a new [`DispatchTable`] on success.
+    /// This leaves the original `dispatch_table` intact, in case of linking errors.
     pub(super) fn relink_all(
         unlinked_assemblies: &mut HashMap<PathBuf, Assembly>,
         linked_assemblies: &mut HashMap<PathBuf, Assembly>,
         dispatch_table: &DispatchTable,
-    ) -> anyhow::Result<DispatchTable> {
-        let mut assemblies = unlinked_assemblies
+        type_table: &TypeTable,
+    ) -> anyhow::Result<(DispatchTable, TypeTable)> {
+        let mut dependencies: HashMap<String, Vec<String>> = unlinked_assemblies
+            .values()
+            .map(|assembly| {
+                let info = &assembly.info;
+                let dependencies: Vec<String> = info.dependencies().map(From::from).collect();
+
+                (info.symbols.path().to_owned(), dependencies)
+            })
+            .filter(|(_, dependencies)| !dependencies.is_empty())
+            .collect();
+
+        // Associate the new assemblies with the old assemblies
+        let mut assemblies_to_link: VecDeque<_> = unlinked_assemblies
             .iter_mut()
-            .map(|(old_path, asm)| {
-                let old_assembly = linked_assemblies.get(old_path);
+            .map(|(old_path, asm)| (linked_assemblies.get(old_path), asm))
+            .collect();
 
-                (asm, old_assembly)
-            })
-            .collect::<Vec<_>>();
+        // Clone the type table, such that we can roll back if linking fails
+        let mut type_table = type_table.clone();
 
         // Clone the dispatch table, such that we can roll back if linking fails
         let mut dispatch_table = dispatch_table.clone();
 
-        // Remove the old assemblies' functions from the dispatch table
-        for (_, old_assembly) in assemblies.iter() {
-            if let Some(assembly) = old_assembly {
-                for function in assembly.info.symbols.functions() {
-                    dispatch_table.remove_fn(function.prototype.name());
-                }
+        while let Some(mut entry) = assemblies_to_link.pop_front() {
+            let (ref old_assembly, ref mut new_assembly) = entry;
+
+            let new_path = new_assembly.info.symbols.path();
+
+            // Are there any dependencies that still need to be loaded?
+            if dependencies.contains_key(new_path) {
+                assemblies_to_link.push_back(entry);
+
+                continue;
             }
-        }
 
-        // Insert all assemblies' functions into the dispatch table
-        for (new_assembly, _) in assemblies.iter() {
-            for function in new_assembly.info().symbols.functions() {
-                dispatch_table.insert_fn(function.prototype.name(), function.clone());
+            let old_types: Option<(&Assembly, Vec<Arc<TypeInfo>>)> =
+                old_assembly.map(|old_assembly| {
+                    // Remove the old assemblies' types from the type table
+                    let old_types = old_assembly
+                        .info
+                        .symbols
+                        .types()
+                        .iter()
+                        .map(|type_info| {
+                            type_table.remove_type_by_id(&type_info.id).expect(
+                                "All types from a loaded assembly must exist in the type table.",
+                            )
+                        })
+                        .collect();
+
+                    (old_assembly, old_types)
+                });
+
+            // Collect all types that need to be loaded
+            let mut types_to_load: VecDeque<&abi::TypeInfo> =
+                new_assembly.info.symbols.types().iter().collect();
+
+            let mut new_types = Vec::with_capacity(types_to_load.len());
+
+            // Load all types, retrying types that depend on other unloaded types within the module
+            while let Some(type_info) = types_to_load.pop_front() {
+                match TypeInfo::try_from_abi(type_info, &type_table) {
+                    Ok(type_info) => {
+                        let type_info = Arc::new(type_info);
+                        new_types.push(type_info.clone());
+                        assert!(type_table.insert_type(type_info).is_none());
+                    }
+                    Err(TryFromAbiError::UnknownTypeId(_)) => {
+                        types_to_load.push_back(type_info);
+                    }
+                }
             }
-        }
 
-        let to_link = assemblies
-            .iter_mut()
-            .flat_map(|(asm, _)| asm.info.dispatch_table.iter_mut())
-            // Only take signatures into account that do *not* yet have a function pointer assigned
-            // by the compiler.
-            .filter(|(ptr, _)| ptr.is_null());
+            let types_to_link = new_assembly
+                .info
+                .type_lut
+                .iter_mut()
+                // Only take signatures into account that do *not* yet have a type handle assigned
+                // by the compiler.
+                .filter(|(_, ptr, _)| ptr.is_null());
+
+            Assembly::link_all_types(&mut type_table, types_to_link)?;
+
+            // Memory map allocated object
+            if let Some((old_assembly, old_types)) = old_types {
+                let mapping = Mapping::new(&old_types, &new_types);
+                let _deleted_objects = old_assembly.allocator.map_memory(mapping);
+                // DISCUSSION: Do we need to maintain an assembly for the type LUT of allocated objects with deleted types?
+            }
 
-        Assembly::link_all_impl(&mut dispatch_table, to_link)?;
+            // Remove the old assembly's functions from the dispatch table
+            if let Some(old_assembly) = old_assembly {
+                dispatch_table.remove_module(&old_assembly.info.symbols);
+            }
 
-        let assemblies_to_map: Vec<_> = assemblies
-            .into_iter()
-            .filter_map(|(new_asm, old_asm)| old_asm.map(|old_asm| (old_asm, new_asm)))
-            .collect();
+            // Insert the new assembly's functions into the dispatch table
+            dispatch_table.insert_module(&new_assembly.info.symbols, &type_table);
 
-        let mut assemblies_to_keep = HashMap::new();
-        for (old_assembly, new_assembly) in assemblies_to_map.iter() {
-            let old_types: Vec<UnsafeTypeInfo> = old_assembly
+            let functions_to_link = new_assembly
                 .info
-                .symbols
-                .types()
-                .iter()
-                .map(|ty| {
-                    // Safety: `ty` is a shared reference, so is guaranteed to not be `ptr::null()`.
-                    UnsafeTypeInfo::new(unsafe {
-                        NonNull::new_unchecked(*ty as *const abi::TypeInfo as *mut _)
-                    })
-                })
-                .collect();
-
-            let new_types: Vec<UnsafeTypeInfo> = new_assembly
-                .info
-                .symbols
-                .types()
-                .iter()
-                .map(|ty| {
-                    // Safety: `ty` is a shared reference, so is guaranteed to not be `ptr::null()`.
-                    UnsafeTypeInfo::new(unsafe {
-                        NonNull::new_unchecked(*ty as *const abi::TypeInfo as *mut _)
-                    })
-                })
-                .collect();
-
-            let mapping = Mapping::new(&old_types, &new_types);
-            let deleted_objects = old_assembly.allocator.map_memory(mapping);
-
-            if !deleted_objects.is_empty() {
-                // Retain the previous assembly
-                assemblies_to_keep.insert(
-                    old_assembly.library_path().to_path_buf(),
-                    new_assembly.library_path().to_path_buf(),
-                );
-            }
+                .dispatch_table
+                .iter_mut()
+                // Only take signatures into account that do *not* yet have a function pointer assigned
+                // by the compiler. When an assembly is compiled it "pre-fills" its internal dispatch
+                // table with pointers to self-referencing functions.
+                .filter(|(ptr, _)| ptr.is_null());
+
+            // Update the dispatch tables of the assemblies themselves based on our global dispatch
+            // table. This will effectively link the function definitions of the assemblies together.
+            // It also modifies the internal state of the assemblies.
+            //
+            // Note that linking may fail because for instance functions remaining unlinked (missing)
+            // or the signature of a function doesnt match.
+            Assembly::link_all_functions(&mut dispatch_table, functions_to_link)?;
+
+            // Remove this assembly from the dependencies
+            dependencies
+                .values_mut()
+                .for_each(|dependencies| dependencies.retain(|path| path != new_path));
+
+            // Remove assemblies that no longer have dependencies
+            dependencies.retain(|_, dependencies| !dependencies.is_empty());
         }
 
         let mut newly_linked = HashMap::new();
         std::mem::swap(unlinked_assemblies, &mut newly_linked);
 
-        for (old_path, mut new_assembly) in newly_linked.into_iter() {
-            let mut old_assembly = linked_assemblies
-                .remove(&old_path)
-                .expect("Assembly must exist.");
-
-            let new_path = if let Some(new_path) = assemblies_to_keep.remove(&old_path) {
-                // Retain all existing legacy libs
-                new_assembly
-                    .legacy_libs
-                    .append(&mut old_assembly.legacy_libs);
-
-                new_assembly.legacy_libs.push(old_assembly.into_library());
-
-                new_path
-            } else {
-                new_assembly.library_path().to_path_buf()
-            };
+        for (old_path, new_assembly) in newly_linked.into_iter() {
+            assert!(
+                linked_assemblies.remove(&old_path).is_some(),
+                "Assembly must exist."
+            );
 
+            let new_path = new_assembly.library_path.clone();
             linked_assemblies.insert(new_path, new_assembly);
         }
 
-        Ok(dispatch_table)
+        Ok((dispatch_table, type_table))
     }
 
     /// Returns the assembly's information.
-    pub fn info(&self) -> &AssemblyInfo {
+    pub fn info(&self) -> &abi::AssemblyInfo {
         &self.info
     }
 
diff --git a/crates/mun_runtime/src/dispatch_table.rs b/crates/mun_runtime/src/dispatch_table.rs
new file mode 100644
index 000000000..892d953d0
--- /dev/null
+++ b/crates/mun_runtime/src/dispatch_table.rs
@@ -0,0 +1,55 @@
+use crate::function_info::FunctionDefinition;
+use memory::type_table::TypeTable;
+use rustc_hash::FxHashMap;
+use std::sync::Arc;
+
+/// A runtime dispatch table that maps full paths to function and struct information.
+#[derive(Clone, Default)]
+pub struct DispatchTable {
+    functions: FxHashMap<String, Arc<FunctionDefinition>>,
+}
+
+impl DispatchTable {
+    /// Retrieves the [`FunctionDefinition`] corresponding to `fn_path`, if it exists.
+    pub fn get_fn(&self, fn_path: &str) -> Option<Arc<FunctionDefinition>> {
+        self.functions.get(fn_path).map(Clone::clone)
+    }
+
+    /// Inserts the `fn_info` for `fn_path` into the dispatch table.
+    ///
+    /// If the dispatch table already contained this `fn_path`, the value is updated, and the old
+    /// value is returned.
+    pub fn insert_fn<S: ToString>(
+        &mut self,
+        fn_path: S,
+        fn_info: Arc<FunctionDefinition>,
+    ) -> Option<Arc<FunctionDefinition>> {
+        self.functions.insert(fn_path.to_string(), fn_info)
+    }
+
+    // /// Removes and returns the `fn_info` corresponding to `fn_path`, if it exists.
+    // pub fn remove_fn<S: AsRef<str>>(&mut self, fn_path: S) -> Option<Arc<FunctionDefinition>> {
+    //     self.functions.remove(fn_path.as_ref())
+    // }
+
+    /// Removes the function definitions from the given assembly from this dispatch table.
+    pub fn remove_module(&mut self, assembly: &abi::ModuleInfo) {
+        for function in assembly.functions() {
+            if let Some(value) = self.functions.get(function.prototype.name()) {
+                if value.fn_ptr == function.fn_ptr {
+                    self.functions.remove(function.prototype.name());
+                }
+            }
+        }
+    }
+
+    /// Add the function definitions from the given assembly from this dispatch table.
+    pub fn insert_module(&mut self, assembly: &abi::ModuleInfo, type_table: &TypeTable) {
+        for fn_def in assembly.functions() {
+            let fn_def = FunctionDefinition::try_from_abi(fn_def, type_table)
+                .expect("All types from a loaded assembly must exist in the type table.");
+
+            self.insert_fn(fn_def.prototype.name.clone(), Arc::new(fn_def));
+        }
+    }
+}
diff --git a/crates/mun_runtime/src/function_info.rs b/crates/mun_runtime/src/function_info.rs
new file mode 100644
index 000000000..dd0a5a5f7
--- /dev/null
+++ b/crates/mun_runtime/src/function_info.rs
@@ -0,0 +1,134 @@
+use memory::{type_table::TypeTable, TryFromAbiError, TypeInfo};
+use std::{ffi::c_void, sync::Arc};
+
+/// A linked version of [`mun_abi::FunctionDefinition`] that has resolved all occurrences of `TypeId` with `TypeInfo`.
+#[derive(Clone)]
+pub struct FunctionDefinition {
+    /// Function prototype
+    pub prototype: FunctionPrototype,
+    /// Function pointer
+    pub fn_ptr: *const c_void,
+}
+
+unsafe impl Send for FunctionDefinition {}
+unsafe impl Sync for FunctionDefinition {}
+
+impl FunctionDefinition {
+    /// Tries to convert from an `abi::FunctionDefinition`.
+    pub fn try_from_abi(
+        fn_def: &abi::FunctionDefinition,
+        type_table: &TypeTable,
+    ) -> Result<Self, TryFromAbiError> {
+        let prototype = FunctionPrototype::try_from_abi(&fn_def.prototype, type_table)?;
+
+        Ok(Self {
+            prototype,
+            fn_ptr: fn_def.fn_ptr,
+        })
+    }
+}
+
+/// A linked version of [`mun_abi::FunctionPrototype`] that has resolved all occurrences of `TypeId` with `TypeInfo`.
+#[derive(Clone)]
+pub struct FunctionPrototype {
+    /// Function name
+    pub name: String,
+    /// The type signature of the function
+    pub signature: FunctionSignature,
+}
+
+impl FunctionPrototype {
+    /// Tries to convert from an `abi::FunctionPrototype`.
+    pub fn try_from_abi(
+        fn_prototype: &abi::FunctionPrototype,
+        type_table: &TypeTable,
+    ) -> Result<Self, TryFromAbiError> {
+        let signature = FunctionSignature::try_from_abi(&fn_prototype.signature, type_table)?;
+
+        Ok(Self {
+            name: fn_prototype.name().to_owned(),
+            signature,
+        })
+    }
+}
+
+/// A linked version of [`mun_abi::FunctionSignature`] that has resolved all occurrences of `TypeId` with `TypeInfo`.
+#[derive(Clone)]
+pub struct FunctionSignature {
+    /// Argument types
+    pub arg_types: Vec<Arc<TypeInfo>>,
+    /// Return type
+    pub return_type: Arc<TypeInfo>,
+}
+
+impl FunctionSignature {
+    /// Tries to convert from an `abi::FunctionSignature`.
+    pub fn try_from_abi(
+        fn_sig: &abi::FunctionSignature,
+        type_table: &TypeTable,
+    ) -> Result<Self, TryFromAbiError> {
+        let arg_types: Vec<Arc<TypeInfo>> = fn_sig
+            .arg_types()
+            .iter()
+            .map(|type_id| {
+                type_table
+                    .find_type_info_by_id(type_id)
+                    .ok_or_else(|| TryFromAbiError::UnknownTypeId(type_id.clone()))
+            })
+            .collect::<Result<_, _>>()?;
+
+        let return_type = type_table
+            .find_type_info_by_id(&fn_sig.return_type)
+            .ok_or_else(|| TryFromAbiError::UnknownTypeId(fn_sig.return_type.clone()))?;
+
+        Ok(Self {
+            arg_types,
+            return_type,
+        })
+    }
+}
+
+/// A value-to-`FunctionDefinition` conversion that consumes the input value.
+pub trait IntoFunctionDefinition {
+    /// Performs the conversion.
+    fn into<S: Into<String>>(self, name: S) -> FunctionDefinition;
+}
+
+macro_rules! into_function_info_impl {
+    ($(
+        extern "C" fn($($T:ident),*) -> $R:ident;
+    )+) => {
+        $(
+            impl<$R: memory::HasStaticTypeInfo, $($T: memory::HasStaticTypeInfo,)*> IntoFunctionDefinition
+            for extern "C" fn($($T),*) -> $R
+            {
+                fn into<S: Into<String>>(self, name: S) -> FunctionDefinition {
+                    FunctionDefinition {
+                        fn_ptr: self as *const std::ffi::c_void,
+                        prototype: FunctionPrototype {
+                            name: name.into(),
+                            signature: FunctionSignature {
+                                arg_types: vec![$(<$T as memory::HasStaticTypeInfo>::type_info().clone(),)*],
+                                return_type: <R as memory::HasStaticTypeInfo>::type_info().clone(),
+                            }
+                        }
+                    }
+                }
+            }
+        )+
+    }
+}
+
+into_function_info_impl! {
+    extern "C" fn() -> R;
+    extern "C" fn(A) -> R;
+    extern "C" fn(A, B) -> R;
+    extern "C" fn(A, B, C) -> R;
+    extern "C" fn(A, B, C, D) -> R;
+    extern "C" fn(A, B, C, D, E) -> R;
+    extern "C" fn(A, B, C, D, E, F) -> R;
+    extern "C" fn(A, B, C, D, E, F, G) -> R;
+    extern "C" fn(A, B, C, D, E, F, G, H) -> R;
+    extern "C" fn(A, B, C, D, E, F, G, H, I) -> R;
+    extern "C" fn(A, B, C, D, E, F, G, H, I, J) -> R;
+}
diff --git a/crates/mun_runtime/src/garbage_collector.rs b/crates/mun_runtime/src/garbage_collector.rs
index c921c7b65..29e95ad1d 100644
--- a/crates/mun_runtime/src/garbage_collector.rs
+++ b/crates/mun_runtime/src/garbage_collector.rs
@@ -1,152 +1,6 @@
-use memory::{
-    gc::{self, HasIndirectionPtr},
-    TypeGroup,
-};
-use std::{alloc::Layout, hash::Hash, ptr::NonNull};
-
-/// `UnsafeTypeInfo` is a type that wraps a `NonNull<TypeInfo>` and indicates unsafe interior
-/// operations on the wrapped `TypeInfo`. The unsafety originates from uncertainty about the
-/// lifetime of the wrapped `TypeInfo`.
-///
-/// Rust lifetime rules do not allow separate lifetimes for struct fields, but we can make `unsafe`
-/// guarantees about their lifetimes. Thus the `UnsafeTypeInfo` type is the only legal way to obtain
-/// shared references to the wrapped `TypeInfo`.
-#[derive(Clone, Copy, Debug)]
-#[repr(transparent)]
-pub struct UnsafeTypeInfo(NonNull<abi::TypeInfo>);
-
-impl UnsafeTypeInfo {
-    /// Constructs a new instance of `UnsafeTypeInfo`, which will wrap the specified `type_info`
-    /// pointer.
-    ///
-    /// All access to the inner value through methods is `unsafe`.
-    pub fn new(type_info: NonNull<abi::TypeInfo>) -> Self {
-        Self(type_info)
-    }
-
-    /// Unwraps the value.
-    pub fn into_inner(self) -> NonNull<abi::TypeInfo> {
-        self.0
-    }
-}
-
-impl PartialEq for UnsafeTypeInfo {
-    fn eq(&self, other: &Self) -> bool {
-        unsafe { *self.0.as_ref() == *other.0.as_ref() }
-    }
-}
-
-impl Eq for UnsafeTypeInfo {}
-
-impl Hash for UnsafeTypeInfo {
-    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-        unsafe { self.0.as_ref().hash(state) };
-    }
-}
-
-impl memory::TypeDesc for UnsafeTypeInfo {
-    fn name(&self) -> &str {
-        unsafe { self.0.as_ref().name() }
-    }
-
-    fn guid(&self) -> &abi::Guid {
-        unsafe { &self.0.as_ref().guid }
-    }
-
-    fn group(&self) -> TypeGroup {
-        TypeGroup::from(unsafe { &self.0.as_ref().data })
-    }
-}
-
-impl memory::TypeFields<UnsafeTypeInfo> for UnsafeTypeInfo {
-    fn fields(&self) -> Vec<(&str, Self)> {
-        if let Some(s) = unsafe { self.0.as_ref().as_struct() } {
-            s.field_names()
-                .zip(s.field_types().iter().map(|ty| {
-                    // Safety: `ty` is a shared reference, so is guaranteed to not be `ptr::null()`.
-                    UnsafeTypeInfo::new(unsafe {
-                        NonNull::new_unchecked(*ty as *const abi::TypeInfo as *mut _)
-                    })
-                }))
-                .collect()
-        } else {
-            Vec::new()
-        }
-    }
-
-    fn offsets(&self) -> &[u16] {
-        if let Some(s) = unsafe { self.0.as_ref().as_struct() } {
-            s.field_offsets()
-        } else {
-            &[]
-        }
-    }
-}
-
-unsafe impl Send for UnsafeTypeInfo {}
-unsafe impl Sync for UnsafeTypeInfo {}
-
-pub struct Trace {
-    obj: GcPtr,
-    ty: UnsafeTypeInfo,
-    index: usize,
-}
-
-impl Iterator for Trace {
-    type Item = GcPtr;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        let struct_ty = unsafe { self.ty.0.as_ref() }.as_struct()?;
-        let field_count = struct_ty.field_types().len();
-        while self.index < field_count {
-            let index = self.index;
-            self.index += 1;
-
-            let field_ty = struct_ty.field_types()[index];
-            if let Some(field_struct_ty) = field_ty.as_struct() {
-                if field_struct_ty.memory_kind == abi::StructMemoryKind::Gc {
-                    let offset = struct_ty.field_offsets()[index];
-                    return Some(unsafe {
-                        *self.obj.deref::<u8>().add(offset as usize).cast::<GcPtr>()
-                    });
-                }
-            }
-        }
-        None
-    }
-}
-
-impl memory::TypeMemory for UnsafeTypeInfo {
-    fn layout(&self) -> Layout {
-        let ty = unsafe { self.0.as_ref() };
-        Layout::from_size_align(ty.size_in_bytes(), ty.alignment())
-            .unwrap_or_else(|_| panic!("invalid layout from Mun Type: {:?}", ty))
-    }
-
-    fn is_stack_allocated(&self) -> bool {
-        unsafe {
-            self.0
-                .as_ref()
-                .as_struct()
-                .map_or(true, |s| s.memory_kind == abi::StructMemoryKind::Value)
-        }
-    }
-}
-
-impl gc::TypeTrace for UnsafeTypeInfo {
-    type Trace = Trace;
-
-    fn trace(&self, obj: GcPtr) -> Self::Trace {
-        Trace {
-            ty: *self,
-            obj,
-            index: 0,
-        }
-    }
-}
+use memory::gc;
 
 /// Defines the garbage collector used by the `Runtime`.
-pub type GarbageCollector = gc::MarkSweep<UnsafeTypeInfo, gc::NoopObserver<gc::Event>>;
+pub type GarbageCollector = gc::MarkSweep<gc::NoopObserver<gc::Event>>;
 
-pub use gc::GcPtr;
-pub type GcRootPtr = gc::GcRootPtr<UnsafeTypeInfo, GarbageCollector>;
+pub type GcRootPtr = gc::GcRootPtr<GarbageCollector>;
diff --git a/crates/mun_runtime/src/lib.rs b/crates/mun_runtime/src/lib.rs
index b6cb6b760..f3a44b3e6 100644
--- a/crates/mun_runtime/src/lib.rs
+++ b/crates/mun_runtime/src/lib.rs
@@ -8,22 +8,28 @@ mod assembly;
 #[macro_use]
 mod garbage_collector;
 mod adt;
+mod dispatch_table;
+mod function_info;
 mod marshal;
 mod reflection;
 
 use anyhow::Result;
+use dispatch_table::DispatchTable;
 use garbage_collector::GarbageCollector;
 use log::{debug, error, info};
-use memory::gc::{self, GcRuntime};
+use memory::{
+    gc::{self, GcRuntime},
+    type_table::TypeTable,
+};
 use mun_project::LOCKFILE_NAME;
 use notify::{RawEvent, RecommendedWatcher, RecursiveMode, Watcher};
-use rustc_hash::FxHashMap;
 use std::{
     collections::{HashMap, VecDeque},
-    ffi, io, mem,
+    ffi,
+    ffi::c_void,
+    fmt::{Debug, Display, Formatter},
+    io, mem,
     path::{Path, PathBuf},
-    ptr::NonNull,
-    string::ToString,
     sync::{
         mpsc::{channel, Receiver},
         Arc,
@@ -33,21 +39,24 @@ use std::{
 pub use crate::{
     adt::{RootedStruct, StructRef},
     assembly::Assembly,
-    garbage_collector::UnsafeTypeInfo,
+    function_info::{
+        FunctionDefinition, FunctionPrototype, FunctionSignature, IntoFunctionDefinition,
+    },
     marshal::Marshal,
     reflection::{ArgumentReflection, ReturnTypeReflection},
 };
-use abi::FunctionSignature;
-pub use abi::IntoFunctionDefinition;
-use std::ffi::c_void;
-use std::fmt::{Debug, Display, Formatter};
+// Re-export some useful types so crates dont have to depend on mun_memory as well.
+use crate::reflection::equals_return_type;
+pub use memory::{FieldInfo, HasStaticTypeInfo, StructInfo, TypeFields, TypeInfo};
 
 /// Options for the construction of a [`Runtime`].
 pub struct RuntimeOptions {
     /// Path to the entry point library
     pub library_path: PathBuf,
+    /// Custom type table used for the runtime
+    pub type_table: TypeTable,
     /// Custom user injected functions
-    pub user_functions: Vec<(abi::FunctionDefinition, abi::FunctionDefinitionStorage)>,
+    pub user_functions: Vec<FunctionDefinition>,
 }
 
 /// Retrieve the allocator using the provided handle.
@@ -60,21 +69,36 @@ unsafe fn get_allocator(alloc_handle: *mut ffi::c_void) -> Arc<GarbageCollector>
     Arc::from_raw(alloc_handle as *const GarbageCollector)
 }
 
+/// Retrieve the `TypeInfo` using the provided handle.
+///
+/// # Safety
+///
+/// The type handle must have been returned from a call to [`Arc<TypeInfo>::into_raw`][into_raw].
+unsafe fn get_type_info(type_handle: *const ffi::c_void) -> Arc<TypeInfo> {
+    Arc::from_raw(type_handle as *const TypeInfo)
+}
+
 extern "C" fn new(
-    type_info: *const abi::TypeInfo,
+    type_handle: *const ffi::c_void,
     alloc_handle: *mut ffi::c_void,
 ) -> *const *mut ffi::c_void {
+    // SAFETY: The runtime always constructs and uses `Arc<TypeInfo>::into_raw` to set the type
+    // type handles in the type LUT.
+    let type_info = unsafe { get_type_info(type_handle) };
+
     // Safety: `new` is only called from within Mun assemblies' core logic, so we are guaranteed
     // that the `Runtime` and its `GarbageCollector` still exist if this function is called, and
     // will continue to do so for the duration of this function.
     let allocator = unsafe { get_allocator(alloc_handle) };
     // Safety: the Mun Compiler guarantees that `new` is never called with `ptr::null()`.
-    let type_info = UnsafeTypeInfo::new(unsafe { NonNull::new_unchecked(type_info as *mut _) });
-    let handle = allocator.alloc(type_info);
+    let handle = allocator.alloc(&type_info);
 
     // Prevent destruction of the allocator
     mem::forget(allocator);
 
+    // Prevent destruction of the type info
+    mem::forget(type_info);
+
     handle.into()
 }
 
@@ -89,13 +113,14 @@ impl RuntimeBuilder {
         Self {
             options: RuntimeOptions {
                 library_path: library_path.into(),
+                type_table: Default::default(),
                 user_functions: Default::default(),
             },
         }
     }
 
     /// Adds a custom user function to the dispatch table.
-    pub fn insert_fn<S: AsRef<str>, F: abi::IntoFunctionDefinition>(
+    pub fn insert_fn<S: Into<String>, F: IntoFunctionDefinition>(
         mut self,
         name: S,
         func: F,
@@ -123,75 +148,6 @@ impl RuntimeBuilder {
     }
 }
 
-type DependencyCounter = usize;
-type Dependency<T> = (T, DependencyCounter);
-type DependencyMap<T> = FxHashMap<String, Dependency<T>>;
-
-/// A runtime dispatch table that maps full paths to function and struct information.
-#[derive(Clone, Default)]
-pub struct DispatchTable {
-    functions: FxHashMap<String, abi::FunctionDefinition>,
-    fn_dependencies: FxHashMap<String, DependencyMap<abi::FunctionPrototype>>,
-}
-
-impl DispatchTable {
-    /// Retrieves the [`abi::FunctionDefinition`] corresponding to `fn_path`, if it exists.
-    pub fn get_fn(&self, fn_path: &str) -> Option<&abi::FunctionDefinition> {
-        self.functions.get(fn_path)
-    }
-
-    /// Inserts the `fn_info` for `fn_path` into the dispatch table.
-    ///
-    /// If the dispatch table already contained this `fn_path`, the value is updated, and the old
-    /// value is returned.
-    pub fn insert_fn<S: ToString>(
-        &mut self,
-        fn_path: S,
-        fn_info: abi::FunctionDefinition,
-    ) -> Option<abi::FunctionDefinition> {
-        self.functions.insert(fn_path.to_string(), fn_info)
-    }
-
-    /// Removes and returns the `fn_info` corresponding to `fn_path`, if it exists.
-    pub fn remove_fn<S: AsRef<str>>(&mut self, fn_path: S) -> Option<abi::FunctionDefinition> {
-        self.functions.remove(fn_path.as_ref())
-    }
-
-    /// Adds `fn_path` from `assembly_path` as a dependency; incrementing its usage counter.
-    pub fn add_fn_dependency<S: ToString, T: ToString>(
-        &mut self,
-        assembly_path: S,
-        fn_path: T,
-        fn_prototype: abi::FunctionPrototype,
-    ) {
-        let dependencies = self
-            .fn_dependencies
-            .entry(assembly_path.to_string())
-            .or_default();
-
-        let (_, counter) = dependencies
-            .entry(fn_path.to_string())
-            .or_insert((fn_prototype, 0));
-
-        *counter += 1;
-    }
-
-    /// Removes `fn_path` from `assembly_path` as a dependency; decrementing its usage counter.
-    pub fn remove_fn_dependency<S: AsRef<str>, T: AsRef<str>>(
-        &mut self,
-        assembly_path: S,
-        fn_path: T,
-    ) {
-        if let Some(dependencies) = self.fn_dependencies.get_mut(assembly_path.as_ref()) {
-            if let Some((key, (fn_sig, counter))) = dependencies.remove_entry(fn_path.as_ref()) {
-                if counter > 1 {
-                    dependencies.insert(key, (fn_sig, counter - 1));
-                }
-            }
-        }
-    }
-}
-
 /// A runtime for the Mun language.
 ///
 /// # Logging
@@ -206,11 +162,11 @@ pub struct Runtime {
     /// Assemblies that have changed and thus need to be relinked. Maps the old to the (potentially) new path.
     assemblies_to_relink: VecDeque<(PathBuf, PathBuf)>,
     dispatch_table: DispatchTable,
+    type_table: TypeTable,
     watcher: RecommendedWatcher,
     watcher_rx: Receiver<RawEvent>,
     renamed_files: HashMap<u32, PathBuf>,
     gc: Arc<GarbageCollector>,
-    _user_functions: Vec<abi::FunctionDefinitionStorage>,
 }
 
 impl Runtime {
@@ -239,29 +195,28 @@ impl Runtime {
         let (tx, rx) = channel();
 
         let mut dispatch_table = DispatchTable::default();
+        let type_table = options.type_table;
 
         // Add internal functions
         options.user_functions.push(IntoFunctionDefinition::into(
-            new as extern "C" fn(*const abi::TypeInfo, *mut ffi::c_void) -> *const *mut ffi::c_void,
+            new as extern "C" fn(*const ffi::c_void, *mut ffi::c_void) -> *const *mut ffi::c_void,
             "new",
         ));
 
-        let mut storages = Vec::with_capacity(options.user_functions.len());
-        for (info, storage) in options.user_functions.into_iter() {
-            dispatch_table.insert_fn(info.prototype.name().to_string(), info);
-            storages.push(storage)
-        }
+        options.user_functions.into_iter().for_each(|fn_def| {
+            dispatch_table.insert_fn(fn_def.prototype.name.clone(), Arc::new(fn_def));
+        });
 
         let watcher: RecommendedWatcher = Watcher::new_raw(tx)?;
         let mut runtime = Runtime {
             assemblies: HashMap::new(),
             assemblies_to_relink: VecDeque::new(),
             dispatch_table,
+            type_table,
             watcher,
             watcher_rx: rx,
             renamed_files: HashMap::new(),
             gc: Arc::new(self::garbage_collector::GarbageCollector::default()),
-            _user_functions: storages,
         };
 
         runtime.add_assembly(&options.library_path)?;
@@ -324,7 +279,8 @@ impl Runtime {
             }
         }
 
-        self.dispatch_table = Assembly::link_all(loaded.values_mut(), &self.dispatch_table)?;
+        (self.dispatch_table, self.type_table) =
+            Assembly::link_all(loaded.values_mut(), &self.dispatch_table, &self.type_table)?;
 
         for (library_path, assembly) in loaded.into_iter() {
             self.watcher
@@ -337,22 +293,19 @@ impl Runtime {
     }
 
     /// Retrieves the function definition corresponding to `function_name`, if available.
-    pub fn get_function_definition(&self, function_name: &str) -> Option<&abi::FunctionDefinition> {
+    pub fn get_function_definition(&self, function_name: &str) -> Option<Arc<FunctionDefinition>> {
         // TODO: Verify that when someone tries to invoke a non-public function, it should fail.
         self.dispatch_table.get_fn(function_name)
     }
 
     /// Retrieves the type definition corresponding to `type_name`, if available.
-    pub fn get_type_info(&self, type_name: &str) -> Option<&abi::TypeInfo> {
-        for assembly in self.assemblies.values() {
-            for type_info in assembly.info().symbols.types().iter() {
-                if type_info.name() == type_name {
-                    return Some(type_info);
-                }
-            }
-        }
+    pub fn get_type_info_by_name(&self, type_name: &str) -> Option<Arc<TypeInfo>> {
+        self.type_table.find_type_info_by_name(type_name)
+    }
 
-        None
+    /// Retrieve the type information corresponding to the `type_id`, if available.
+    pub fn get_type_info_by_id(&self, type_id: &abi::TypeId) -> Option<Arc<TypeInfo>> {
+        self.type_table.find_type_info_by_id(type_id)
     }
 
     /// Updates the state of the runtime. This includes checking for file changes, and reloading
@@ -374,7 +327,9 @@ impl Runtime {
             path.file_name().expect("Invalid file path.") == LOCKFILE_NAME
         }
 
-        unsafe fn relink_assemblies(runtime: &mut Runtime) -> anyhow::Result<DispatchTable> {
+        unsafe fn relink_assemblies(
+            runtime: &mut Runtime,
+        ) -> anyhow::Result<(DispatchTable, TypeTable)> {
             let mut loaded = HashMap::new();
             let to_load = &mut runtime.assemblies_to_relink;
 
@@ -421,6 +376,7 @@ impl Runtime {
                 &mut loaded,
                 &mut runtime.assemblies,
                 &runtime.dispatch_table,
+                &runtime.type_table,
             )
         }
 
@@ -436,10 +392,11 @@ impl Runtime {
                         debug!("Lockfile deleted");
 
                         match relink_assemblies(self) {
-                            Ok(table) => {
+                            Ok((dispatch_table, type_table)) => {
                                 info!("Succesfully reloaded assemblies.");
 
-                                self.dispatch_table = table;
+                                self.dispatch_table = dispatch_table;
+                                self.type_table = type_table;
                                 self.assemblies_to_relink.clear();
 
                                 return true;
@@ -475,7 +432,7 @@ impl Runtime {
     ///
     /// We cannot return an `Arc` here, because the lifetime of data contained in `GarbageCollector`
     /// is dependent on the `Runtime`.
-    pub fn gc(&self) -> &dyn GcRuntime<UnsafeTypeInfo> {
+    pub fn gc(&self) -> &dyn GcRuntime {
         self.gc.as_ref()
     }
 
@@ -594,24 +551,23 @@ seq_macro::seq!(I in 0..N {
     impl<'arg, #(T~I: ArgumentReflection + Marshal<'arg>,)*> InvokeArgs for (#(T~I,)*) {
         #[allow(unused_variables)]
         fn can_invoke<'runtime>(&self, runtime: &'runtime Runtime, signature: &FunctionSignature) -> Result<(), String> {
-            let arg_types = signature.arg_types();
+            let arg_types = &signature.arg_types;
 
             // Ensure the number of arguments match
             #[allow(clippy::len_zero)]
             if N != arg_types.len() {
-                return Err(format!("invalid return type. Expected: {}. Found: {}", N, arg_types.len()))
+                return Err(format!("Invalid return type. Expected: {}. Found: {}", N, arg_types.len()))
             }
 
             #(
-            crate::reflection::equals_argument_type(runtime, arg_types[I], &self.I)
-                .map_err(|(expected, found)| {
-                    format!(
-                        "invalid argument type at index {}. Expected: {}. Found: {}.",
-                        I,
-                        expected,
-                        found,
-                    )
-                })?;
+            if arg_types[I].id != self.I.type_id(runtime) {
+                return Err(format!(
+                    "Invalid argument type at index {}. Expected: {}. Found: {}.",
+                    I,
+                    self.I.type_info(runtime).name,
+                    arg_types[I].name,
+                ));
+            }
             )*
 
             Ok(())
@@ -672,27 +628,17 @@ impl Runtime {
         };
 
         // Validate the return type
-        match if let Some(return_type) = function_info.prototype.signature.return_type() {
-            crate::reflection::equals_return_type::<ReturnType>(return_type)
-        } else if <() as ReturnTypeReflection>::type_guid() != ReturnType::type_guid() {
-            Err((
-                <() as ReturnTypeReflection>::type_name(),
-                ReturnType::type_name(),
-            ))
-        } else {
-            Ok(())
-        } {
-            Ok(_) => {}
-            Err((expected, found)) => {
-                return Err(InvokeErr {
-                    msg: format!(
-                        "invalid return type. Expected: {}. Found: {}",
-                        expected, found,
-                    ),
-                    function_name,
-                    arguments,
-                })
-            }
+        if let Err((got, expected)) =
+            equals_return_type::<ReturnType>(&function_info.prototype.signature.return_type)
+        {
+            return Err(InvokeErr {
+                msg: format!(
+                    "unexpected return type, got '{}', expected '{}",
+                    expected, got
+                ),
+                function_name,
+                arguments,
+            });
         }
 
         let result: ReturnType::MunType = unsafe { arguments.invoke(function_info.fn_ptr) };
diff --git a/crates/mun_runtime/src/marshal.rs b/crates/mun_runtime/src/marshal.rs
index b86b184e9..8221e62ab 100644
--- a/crates/mun_runtime/src/marshal.rs
+++ b/crates/mun_runtime/src/marshal.rs
@@ -1,5 +1,7 @@
+use memory::TypeInfo;
+
 use crate::Runtime;
-use std::ptr::NonNull;
+use std::{ptr::NonNull, sync::Arc};
 
 /// Used to do value-to-value conversions that require runtime type information while consuming the
 /// input value.
@@ -22,12 +24,12 @@ pub trait Marshal<'t>: Sized {
     fn marshal_from_ptr<'r>(
         ptr: NonNull<Self::MunType>,
         runtime: &'r Runtime,
-        type_info: Option<&abi::TypeInfo>,
+        type_info: &Arc<TypeInfo>,
     ) -> Self
     where
         Self: 't,
         'r: 't;
 
     /// Marshals `value` to memory location `ptr` (i.e. Rust -> Mun).
-    fn marshal_to_ptr(value: Self, ptr: NonNull<Self::MunType>, type_info: Option<&abi::TypeInfo>);
+    fn marshal_to_ptr(value: Self, ptr: NonNull<Self::MunType>, type_info: &Arc<TypeInfo>);
 }
diff --git a/crates/mun_runtime/src/reflection.rs b/crates/mun_runtime/src/reflection.rs
index 496db48e9..4910371cf 100644
--- a/crates/mun_runtime/src/reflection.rs
+++ b/crates/mun_runtime/src/reflection.rs
@@ -1,32 +1,19 @@
 use crate::{marshal::Marshal, Runtime, StructRef};
-use abi::HasStaticTypeInfo;
-use once_cell::sync::OnceCell;
-
-/// Returns whether the specified argument type matches the `type_info`.
-pub fn equals_argument_type<'e, 'f, T: ArgumentReflection>(
-    runtime: &'f Runtime,
-    type_info: &'e abi::TypeInfo,
-    arg: &'f T,
-) -> Result<(), (&'e str, &'f str)> {
-    if type_info.guid != arg.type_guid(runtime) {
-        Err((type_info.name(), arg.type_name(runtime)))
-    } else {
-        Ok(())
-    }
-}
+use memory::{HasStaticTypeInfo, TypeInfo, TypeInfoData};
+use std::sync::Arc;
 
 /// Returns whether the specified return type matches the `type_info`.
 pub fn equals_return_type<T: ReturnTypeReflection>(
-    type_info: &abi::TypeInfo,
+    type_info: &TypeInfo,
 ) -> Result<(), (&str, &str)> {
     match type_info.data {
-        abi::TypeInfoData::Primitive => {
-            if type_info.guid != T::type_guid() {
-                return Err((type_info.name(), T::type_name()));
+        TypeInfoData::Primitive => {
+            if type_info.id != T::type_id() {
+                return Err((&type_info.name, T::type_name()));
             }
         }
-        abi::TypeInfoData::Struct(_) => {
-            if <StructRef as ReturnTypeReflection>::type_guid() != T::type_guid() {
+        TypeInfoData::Struct(_) => {
+            if <StructRef as ReturnTypeReflection>::type_id() != T::type_id() {
                 return Err(("struct", T::type_name()));
             }
         }
@@ -36,8 +23,8 @@ pub fn equals_return_type<T: ReturnTypeReflection>(
 
 /// A type to emulate dynamic typing across compilation units for static types.
 pub trait ReturnTypeReflection: Sized {
-    /// Retrieves the type's `Guid`.
-    fn type_guid() -> abi::Guid;
+    /// Retrieves the type's `TypeId`.
+    fn type_id() -> abi::TypeId;
 
     /// Retrieves the type's name.
     fn type_name() -> &'static str;
@@ -45,33 +32,31 @@ pub trait ReturnTypeReflection: Sized {
 
 /// A type to emulate dynamic typing across compilation units for statically typed values.
 pub trait ArgumentReflection: Sized {
-    /// Retrieves the `Guid` of the value's type.
-    fn type_guid(&self, runtime: &Runtime) -> abi::Guid;
+    /// Retrieves the argument's type information.
+    fn type_info(&self, runtime: &Runtime) -> Arc<TypeInfo>;
 
-    /// Retrieves the name of the value's type.
-    fn type_name<'r>(&'r self, runtime: &'r Runtime) -> &'r str;
+    /// Retrieves the argument's type ID.
+    fn type_id(&self, runtime: &Runtime) -> abi::TypeId {
+        self.type_info(runtime).id.clone()
+    }
 }
 
 macro_rules! impl_primitive_type {
     ($($ty:ty),+) => {
         $(
             impl ArgumentReflection for $ty {
-                fn type_guid(&self, _runtime: &Runtime) -> abi::Guid {
-                    Self::type_info().guid
-                }
-
-                fn type_name(&self, _runtime: &Runtime) -> &str {
-                    Self::type_info().name()
+                fn type_info(&self, _runtime: &Runtime) -> Arc<TypeInfo> {
+                    <Self as HasStaticTypeInfo>::type_info().clone()
                 }
             }
 
             impl ReturnTypeReflection for $ty {
-                fn type_guid() -> abi::Guid {
-                    Self::type_info().guid
+                fn type_id() -> abi::TypeId {
+                    <Self as abi::HasStaticTypeInfo>::type_info().id.clone()
                 }
 
                 fn type_name() -> &'static str {
-                    Self::type_info().name()
+                    <Self as abi::HasStaticTypeInfo>::type_info().name()
                 }
             }
 
@@ -93,7 +78,7 @@ macro_rules! impl_primitive_type {
                 fn marshal_from_ptr<'r>(
                     ptr: std::ptr::NonNull<Self::MunType>,
                     _runtime: &'r Runtime,
-                    _type_info: Option<&abi::TypeInfo>,
+                    _type_info: &Arc<TypeInfo>,
                 ) -> Self
                 where
                     Self: 't,
@@ -107,7 +92,7 @@ macro_rules! impl_primitive_type {
                 fn marshal_to_ptr(
                     value: Self,
                     mut ptr: std::ptr::NonNull<Self::MunType>,
-                    _type_info: Option<&abi::TypeInfo>,
+                    _type_info: &Arc<TypeInfo>,
                 ) {
                     unsafe { *ptr.as_mut() = value };
                 }
@@ -117,78 +102,43 @@ macro_rules! impl_primitive_type {
 }
 
 impl_primitive_type!(
-    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool
+    i8,
+    i16,
+    i32,
+    i64,
+    i128,
+    isize,
+    u8,
+    u16,
+    u32,
+    u64,
+    u128,
+    usize,
+    f32,
+    f64,
+    bool,
+    ()
 );
 
-impl ReturnTypeReflection for () {
-    fn type_name() -> &'static str {
-        "core::empty"
-    }
-
-    fn type_guid() -> abi::Guid {
-        // TODO: Once `const_fn` lands, replace this with a const md5 hash
-        static GUID: OnceCell<abi::Guid> = OnceCell::new();
-        *GUID.get_or_init(|| abi::Guid(md5::compute(Self::type_name()).0))
-    }
-}
-impl<'t> Marshal<'t> for () {
-    type MunType = ();
-
-    fn marshal_from<'r>(value: Self::MunType, _runtime: &'r Runtime) -> Self
-    where
-        Self: 't,
-        'r: 't,
-    {
-        value
-    }
-
-    fn marshal_into<'r>(self) -> Self::MunType {
-        self
-    }
-
-    fn marshal_from_ptr<'r>(
-        _ptr: std::ptr::NonNull<Self::MunType>,
-        _runtime: &'r Runtime,
-        _type_info: Option<&abi::TypeInfo>,
-    ) -> Self
-    where
-        Self: 't,
-        'r: 't,
-    {
-    }
-
-    fn marshal_to_ptr(
-        _value: Self,
-        mut ptr: std::ptr::NonNull<Self::MunType>,
-        _type_info: Option<&abi::TypeInfo>,
-    ) {
-        unsafe { *ptr.as_mut() = () };
-    }
-}
-
 impl<T> ArgumentReflection for *const T
 where
     *const T: HasStaticTypeInfo,
 {
-    fn type_guid(&self, _runtime: &Runtime) -> abi::Guid {
-        Self::type_info().guid
-    }
-
-    fn type_name(&self, _runtime: &Runtime) -> &str {
-        Self::type_info().name()
+    fn type_info(&self, _runtime: &Runtime) -> Arc<TypeInfo> {
+        <Self as HasStaticTypeInfo>::type_info().clone()
     }
 }
 
 impl<T> ReturnTypeReflection for *const T
 where
-    *const T: HasStaticTypeInfo,
+    *const T: abi::HasStaticTypeInfo,
 {
-    fn type_guid() -> abi::Guid {
-        Self::type_info().guid
+    fn type_id() -> abi::TypeId {
+        <Self as abi::HasStaticTypeInfo>::type_info().id.clone()
     }
 
     fn type_name() -> &'static str {
-        Self::type_info().name()
+        <Self as abi::HasStaticTypeInfo>::type_info().name()
     }
 }
 
@@ -196,24 +146,20 @@ impl<T> ArgumentReflection for *mut T
 where
     *mut T: HasStaticTypeInfo,
 {
-    fn type_guid(&self, _runtime: &Runtime) -> abi::Guid {
-        Self::type_info().guid
-    }
-
-    fn type_name(&self, _runtime: &Runtime) -> &str {
-        Self::type_info().name()
+    fn type_info(&self, _runtime: &Runtime) -> Arc<TypeInfo> {
+        <Self as HasStaticTypeInfo>::type_info().clone()
     }
 }
 
 impl<T> ReturnTypeReflection for *mut T
 where
-    *mut T: HasStaticTypeInfo,
+    *mut T: abi::HasStaticTypeInfo,
 {
-    fn type_guid() -> abi::Guid {
-        Self::type_info().guid
+    fn type_id() -> abi::TypeId {
+        <Self as abi::HasStaticTypeInfo>::type_info().id.clone()
     }
 
     fn type_name() -> &'static str {
-        Self::type_info().name()
+        <Self as abi::HasStaticTypeInfo>::type_info().name()
     }
 }
diff --git a/crates/mun_runtime_capi/Cargo.toml b/crates/mun_runtime_capi/Cargo.toml
index 931b65551..622dadc7f 100644
--- a/crates/mun_runtime_capi/Cargo.toml
+++ b/crates/mun_runtime_capi/Cargo.toml
@@ -17,13 +17,9 @@ name = "mun_runtime"
 crate-type = ["cdylib"]
 
 [dependencies]
-anyhow = "1.0.31"
-lazy_static = "1.4.0"
 abi = { version = "=0.3.0", path = "../mun_abi", package = "mun_abi" }
 memory = { version = "=0.2.0", path = "../mun_memory", package = "mun_memory" }
 runtime = { version = "=0.3.0", path = "../mun_runtime", package = "mun_runtime" }
-parking_lot = "0.12.0"
-rand = "0.8.5"
 
 [dev-dependencies]
 compiler = { path="../mun_compiler", package = "mun_compiler" }
diff --git a/crates/mun_runtime_capi/src/error.rs b/crates/mun_runtime_capi/src/error.rs
index 805cd3278..21325ab07 100644
--- a/crates/mun_runtime_capi/src/error.rs
+++ b/crates/mun_runtime_capi/src/error.rs
@@ -1,45 +1,36 @@
 //! Exposes error reporting using the C ABI.
 
-use std::ffi::CString;
-use std::hash::Hash;
-use std::os::raw::c_char;
-use std::ptr;
+use std::{ffi::CString, os::raw::c_char, ptr};
 
-use crate::hub::HUB;
-use crate::{Token, TypedHandle};
-
-/// A C-style handle to an error.
 #[repr(C)]
-#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq)]
-pub struct ErrorHandle(Token);
-
-impl TypedHandle for ErrorHandle {
-    fn new(token: Token) -> Self {
-        Self(token)
-    }
-
-    fn token(&self) -> Token {
-        self.0
+#[derive(Clone, Copy)]
+/// A C-style handle to an error message.
+///
+/// If the handle contains a non-null pointer, an error occurred.
+pub struct ErrorHandle(pub *const c_char);
+
+impl ErrorHandle {
+    /// Constructs an `ErrorHandle` from the specified error message.
+    pub(crate) fn new<T: Into<Vec<u8>>>(error_message: T) -> Self {
+        let error_message = CString::new(error_message).expect("Invalid error message");
+        Self(CString::into_raw(error_message))
     }
 }
 
-/// Destructs the error corresponding to `error_handle`.
-#[no_mangle]
-pub extern "C" fn mun_error_destroy(error_handle: ErrorHandle) {
-    // If an error exists, destroy it
-    let _error = HUB.errors.unregister(error_handle);
+impl Default for ErrorHandle {
+    fn default() -> Self {
+        Self(ptr::null())
+    }
 }
 
-/// Retrieves the error message corresponding to `error_handle`. If the `error_handle` exists, a
-/// valid `char` pointer is returned, otherwise a null-pointer is returned.
+/// Destructs the error message corresponding to the specified handle.
+///
+/// # Safety
+///
+/// Only call this function on an ErrorHandle once.
 #[no_mangle]
-pub extern "C" fn mun_error_message(error_handle: ErrorHandle) -> *const c_char {
-    let errors = HUB.errors.get_data();
-    let error = match errors.get(&error_handle) {
-        Some(error) => error,
-        None => return ptr::null_mut(),
-    };
-
-    let message = format!("{}", error);
-    CString::new(message).unwrap().into_raw() as *const _
+pub unsafe extern "C" fn mun_error_destroy(error: ErrorHandle) {
+    if !error.0.is_null() {
+        let _drop = CString::from_raw(error.0 as *mut c_char);
+    }
 }
diff --git a/crates/mun_runtime_capi/src/field_info.rs b/crates/mun_runtime_capi/src/field_info.rs
new file mode 100644
index 000000000..54340f5b0
--- /dev/null
+++ b/crates/mun_runtime_capi/src/field_info.rs
@@ -0,0 +1,305 @@
+//! Exposes field information using the C ABI.
+
+use crate::{error::ErrorHandle, type_info::TypeInfoHandle};
+use memory::FieldInfo;
+use std::{
+    ffi::{c_void, CString},
+    os::raw::c_char,
+    ptr,
+    sync::Arc,
+};
+
+/// A C-style handle to a `FieldInfo`.
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct FieldInfoHandle(pub *const c_void);
+
+impl FieldInfoHandle {
+    /// A null handle.
+    pub fn null() -> Self {
+        Self(ptr::null())
+    }
+}
+
+/// A C-style handle to an array of `FieldInfoHandle`s.
+#[repr(C)]
+pub struct FieldInfoSpan {
+    /// Pointer to the start of the array buffer
+    pub data: *const FieldInfoHandle,
+    /// Length of the array (and capacity)
+    pub len: usize,
+}
+
+impl FieldInfoSpan {
+    /// An empty span.
+    pub fn empty() -> Self {
+        Self {
+            data: ptr::null(),
+            len: 0,
+        }
+    }
+}
+
+/// Retrieves the field's name.
+///
+/// # Safety
+///
+/// The caller is responsible for calling `mun_string_destroy` on the return pointer - if it is not null.
+///
+/// This function might result in undefined behavior if the [`crate::TypeInfoHandle`] associated
+/// with this `FieldInfoHandle` has been deallocated.
+#[no_mangle]
+pub unsafe extern "C" fn mun_field_info_name(field_info: FieldInfoHandle) -> *const c_char {
+    let field_info = match (field_info.0 as *const FieldInfo).as_ref() {
+        Some(field_info) => field_info,
+        None => return ptr::null(),
+    };
+
+    CString::new(field_info.name.clone()).unwrap().into_raw() as *const _
+}
+
+/// Retrieves the field's type.
+///
+/// # Safety
+///
+/// This method is considered unsafe because the passed `field_info` might have been deallocated by
+/// a call to [`mun_type_info_decrement_strong_count`] of the type that contains this field.
+#[no_mangle]
+pub unsafe extern "C" fn mun_field_info_type(field_info: FieldInfoHandle) -> TypeInfoHandle {
+    let field_info = match (field_info.0 as *const FieldInfo).as_ref() {
+        Some(field_info) => field_info,
+        None => return TypeInfoHandle::null(),
+    };
+
+    TypeInfoHandle(Arc::into_raw(field_info.type_info.clone()) as *const c_void)
+}
+
+/// Retrieves the field's offset.
+///
+/// # Safety
+///
+/// This method is considered unsafe because the passed `field_info` might have been deallocated by
+/// a call to [`mun_type_info_decrement_strong_count`] of the type that contains this field.
+#[no_mangle]
+pub unsafe extern "C" fn mun_field_info_offset(
+    field_info: FieldInfoHandle,
+    field_offset: *mut u16,
+) -> ErrorHandle {
+    let field_info = match (field_info.0 as *const FieldInfo).as_ref() {
+        Some(field_info) => field_info,
+        None => return ErrorHandle::new("Invalid argument: 'field_info' is null pointer."),
+    };
+
+    let field_offset = match field_offset.as_mut() {
+        Some(field_offset) => field_offset,
+        None => return ErrorHandle::new("Invalid argument: 'field_offset' is null pointer."),
+    };
+
+    *field_offset = field_info.offset;
+
+    ErrorHandle::default()
+}
+
+/// Deallocates a span of `FieldInfo`s that was allocated by the runtime.
+///
+/// Deallocating span only deallocates the data allocated for the span. Deallocating a span will not
+/// deallocate the FieldInfo's it references. `FieldInfo`s are destroyed when the top-level
+/// `TypeInfo` is destroyed.
+///
+/// # Safety
+///
+/// This function receives a span as parameter. Only when the spans data pointer is not null, is the
+/// content deallocated. Passing pointers to invalid data of memory allocated by other processes,
+/// will lead to undefined behavior.
+#[no_mangle]
+pub unsafe extern "C" fn mun_field_info_span_destroy(span: FieldInfoSpan) -> bool {
+    if span.data.is_null() {
+        return false;
+    }
+
+    let data = span.data as *mut *const FieldInfo;
+    let _types = Vec::from_raw_parts(data, span.len, span.len);
+
+    true
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        error::mun_error_destroy,
+        mun_string_destroy,
+        runtime::{mun_runtime_get_type_info_by_name, RuntimeHandle},
+        struct_info::mun_struct_info_fields,
+        test_util::TestDriver,
+        type_info::{mun_type_info_data, mun_type_info_id},
+    };
+    use memory::HasStaticTypeInfo;
+    use std::{
+        ffi::{CStr, CString},
+        mem::MaybeUninit,
+        slice,
+    };
+
+    fn get_first_field<T: Into<Vec<u8>>>(runtime: RuntimeHandle, type_name: T) -> FieldInfoHandle {
+        let type_name = CString::new(type_name).expect("Invalid type name");
+        let mut has_type_info = false;
+        let mut type_info = MaybeUninit::uninit();
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_name(
+                runtime,
+                type_name.as_ptr(),
+                &mut has_type_info as *mut bool,
+                type_info.as_mut_ptr(),
+            )
+        };
+        assert_eq!(handle.0, ptr::null());
+        assert!(has_type_info);
+
+        let type_info = unsafe { type_info.assume_init() };
+
+        let mut type_info_data = MaybeUninit::uninit();
+        let handle = unsafe { mun_type_info_data(type_info, type_info_data.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let type_info_data = unsafe { type_info_data.assume_init() };
+        let struct_info = type_info_data
+            .as_struct()
+            .expect("Type was expected to be a struct.");
+
+        let mut fields = MaybeUninit::uninit();
+        let handle = unsafe { mun_struct_info_fields(struct_info, fields.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let fields = unsafe { fields.assume_init() };
+        assert!(fields.len > 0);
+
+        let field_slice = unsafe { slice::from_raw_parts(fields.data, fields.len) };
+        let first_field = field_slice[0];
+
+        assert!(unsafe { mun_field_info_span_destroy(fields) });
+
+        first_field
+    }
+
+    #[test]
+    fn test_field_info_name_invalid_handle() {
+        let name = unsafe { mun_field_info_name(FieldInfoHandle::null()) };
+        assert_eq!(name, ptr::null());
+    }
+
+    #[test]
+    fn test_field_info_name() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo {
+                a: i32,
+            }
+    "#,
+        );
+
+        let field_info = get_first_field(driver.runtime, "Foo");
+        let name = unsafe { mun_field_info_name(field_info) };
+        assert_ne!(name, ptr::null());
+
+        let name_str = unsafe { CStr::from_ptr(name) }
+            .to_str()
+            .expect("Invalid field name.");
+
+        assert_eq!(name_str, "a");
+
+        unsafe { mun_string_destroy(name) };
+    }
+    #[test]
+    fn test_field_info_type_invalid_handle() {
+        let type_info = unsafe { mun_field_info_type(FieldInfoHandle::null()) };
+        assert_eq!(type_info.0, ptr::null());
+    }
+
+    #[test]
+    fn test_field_info_type() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo {
+                a: i32,
+            }
+    "#,
+        );
+
+        let field_info = get_first_field(driver.runtime, "Foo");
+        let type_info = unsafe { mun_field_info_type(field_info) };
+        assert_ne!(type_info.0, ptr::null());
+
+        let mut type_id = MaybeUninit::uninit();
+        let handle = unsafe { mun_type_info_id(type_info, type_id.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let type_id = unsafe { type_id.assume_init() };
+        assert_eq!(type_id, <i32>::type_info().id);
+    }
+
+    #[test]
+    fn test_field_info_offset_invalid_field_info() {
+        let mut field_offset = MaybeUninit::uninit();
+        let handle =
+            unsafe { mun_field_info_offset(FieldInfoHandle::null(), field_offset.as_mut_ptr()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'field_info' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_field_info_offset_invalid_field_offset() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo {
+                a: i32,
+            }
+    "#,
+        );
+
+        let field_info = get_first_field(driver.runtime, "Foo");
+
+        let handle = unsafe { mun_field_info_offset(field_info, ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'field_offset' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_field_info_offset() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo {
+                a: i32,
+            }
+    "#,
+        );
+
+        let field_info = get_first_field(driver.runtime, "Foo");
+
+        let mut field_offset = MaybeUninit::uninit();
+        let handle = unsafe { mun_field_info_offset(field_info, field_offset.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let field_offset = unsafe { field_offset.assume_init() };
+        assert_eq!(field_offset, 0);
+    }
+
+    #[test]
+    fn test_field_info_span_destroy_empty() {
+        assert!(!unsafe { mun_field_info_span_destroy(FieldInfoSpan::empty()) });
+    }
+}
diff --git a/crates/mun_runtime_capi/src/function_info.rs b/crates/mun_runtime_capi/src/function_info.rs
new file mode 100644
index 000000000..9c5817a5d
--- /dev/null
+++ b/crates/mun_runtime_capi/src/function_info.rs
@@ -0,0 +1,440 @@
+//! Exposes function information using the C ABI.
+
+use crate::{
+    error::ErrorHandle,
+    type_info::{TypeInfoHandle, TypeInfoSpan},
+};
+use memory::TypeInfo;
+use runtime::FunctionDefinition;
+use std::{
+    ffi::{c_void, CString},
+    mem,
+    os::raw::c_char,
+    ptr,
+    sync::Arc,
+};
+
+/// A C-style handle to a `FunctionInfo`.
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct FunctionInfoHandle(pub *const c_void);
+
+impl FunctionInfoHandle {
+    /// A null handle.
+    pub fn null() -> Self {
+        Self(ptr::null())
+    }
+}
+
+/// Decrements the strong count of the `Arc<FunctionDefinition>` associated with `handle`.
+///
+/// # Safety
+///
+/// This function might be unsafe if the underlying data has already been deallocated by a previous
+/// call to [`mun_function_info_decrement_strong_count`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_function_info_decrement_strong_count(
+    fn_info: FunctionInfoHandle,
+) -> bool {
+    if !fn_info.0.is_null() {
+        Arc::decrement_strong_count(fn_info.0);
+        return true;
+    }
+
+    false
+}
+
+/// Increments the strong count of the `Arc<FunctionDefinition>` associated with `handle`.
+///
+/// # Safety
+///
+/// This function might be unsafe if the underlying data has been deallocated by a previous call
+/// to [`mun_function_info_decrement_strong_count`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_function_info_increment_strong_count(
+    fn_info: FunctionInfoHandle,
+) -> bool {
+    if !fn_info.0.is_null() {
+        Arc::increment_strong_count(fn_info.0);
+        return true;
+    }
+
+    false
+}
+
+/// Retrieves the function's function pointer.
+///
+/// # Safety
+///
+/// This function might be unsafe if the underlying data has been deallocated by a previous call
+/// to [`mun_function_info_decrement_strong_count`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_function_info_fn_ptr(fn_info: FunctionInfoHandle) -> *const c_void {
+    let fn_def = match (fn_info.0 as *const FunctionDefinition).as_ref() {
+        Some(fn_def) => fn_def,
+        None => return ptr::null(),
+    };
+
+    fn_def.fn_ptr
+}
+
+/// Retrieves the function's name.
+///
+/// # Safety
+///
+/// The caller is responsible for calling `mun_string_destroy` on the return pointer - if it is not null.
+///
+/// This function might be unsafe if the underlying data has been deallocated by a previous call
+/// to [`mun_function_info_decrement_strong_count`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_function_info_name(fn_info: FunctionInfoHandle) -> *const c_char {
+    let fn_def = match (fn_info.0 as *const FunctionDefinition).as_ref() {
+        Some(fn_def) => fn_def,
+        None => return ptr::null(),
+    };
+
+    CString::new(fn_def.prototype.name.clone())
+        .unwrap()
+        .into_raw() as *const _
+}
+
+/// Retrieves the function's argument types.
+///
+/// # Safety
+///
+/// If a non-null handle is returned, the caller is responsible for calling
+/// `mun_type_info_span_destroy` on the returned handle.
+///
+/// This function might be unsafe if the underlying data has been deallocated by a previous call
+/// to [`mun_function_info_decrement_strong_count`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_function_info_argument_types(
+    fn_info: FunctionInfoHandle,
+    arg_types: *mut TypeInfoSpan,
+) -> ErrorHandle {
+    let fn_info = match (fn_info.0 as *const FunctionDefinition).as_ref() {
+        Some(fn_info) => fn_info,
+        None => return ErrorHandle::new("Invalid argument: 'fn_info' is null pointer."),
+    };
+
+    let arg_types = match arg_types.as_mut() {
+        Some(arg_types) => arg_types,
+        None => return ErrorHandle::new("Invalid argument: 'arg_types' is null pointer."),
+    };
+
+    let fn_sig = &fn_info.prototype.signature;
+    let mut types: Vec<*const TypeInfo> = fn_sig
+        .arg_types
+        .iter()
+        .map(|ty| Arc::into_raw(ty.clone()))
+        .collect();
+
+    arg_types.data = if !types.is_empty() {
+        types.shrink_to_fit();
+        types.as_ptr() as *const _
+    } else {
+        ptr::null()
+    };
+    arg_types.len = types.len();
+
+    // Move ownership to caller
+    mem::forget(types);
+
+    ErrorHandle::default()
+}
+
+/// Retrieves the function's return type.
+///
+/// # Safety
+///
+/// This function might be unsafe if the underlying data has been deallocated by a previous call
+/// to [`mun_function_info_decrement_strong_count`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_function_info_return_type(
+    fn_info: FunctionInfoHandle,
+) -> TypeInfoHandle {
+    let fn_info = match (fn_info.0 as *const FunctionDefinition).as_ref() {
+        Some(fn_info) => fn_info,
+        None => return TypeInfoHandle::null(),
+    };
+
+    TypeInfoHandle(Arc::into_raw(fn_info.prototype.signature.return_type.clone()) as *const c_void)
+}
+
+#[cfg(test)]
+pub(crate) mod tests {
+    use super::*;
+    use crate::{
+        error::mun_error_destroy,
+        mun_string_destroy,
+        runtime::{mun_runtime_get_function_info, RuntimeHandle},
+        test_util::TestDriver,
+        type_info::mun_type_info_id,
+    };
+    use memory::HasStaticTypeInfo;
+    use runtime::FunctionDefinition;
+    use std::{
+        ffi::{CStr, CString},
+        mem::{self, MaybeUninit},
+        sync::Arc,
+    };
+
+    pub(crate) fn get_fake_function_info<T: Into<Vec<u8>>>(
+        runtime: RuntimeHandle,
+        fn_name: T,
+    ) -> FunctionInfoHandle {
+        let fn_name = CString::new(fn_name).expect("Invalid function name");
+        let mut has_fn_info = false;
+        let mut fn_definition = MaybeUninit::uninit();
+        let handle = unsafe {
+            mun_runtime_get_function_info(
+                runtime,
+                fn_name.as_ptr(),
+                &mut has_fn_info as *mut _,
+                fn_definition.as_mut_ptr(),
+            )
+        };
+        assert_eq!(handle.0, ptr::null());
+        assert!(has_fn_info);
+
+        unsafe { fn_definition.assume_init() }
+    }
+
+    #[test]
+    fn test_function_info_decrement_strong_count_invalid_fn_info() {
+        assert_eq!(
+            unsafe { mun_function_info_decrement_strong_count(FunctionInfoHandle::null()) },
+            false
+        );
+    }
+
+    #[test]
+    fn test_function_info_decrement_strong_count() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let fn_info = get_fake_function_info(driver.runtime, "main");
+
+        let fn_info_arc = unsafe { Arc::from_raw(fn_info.0 as *const FunctionDefinition) };
+        let strong_count = Arc::strong_count(&fn_info_arc);
+        assert!(strong_count > 0);
+
+        assert_eq!(
+            unsafe { mun_function_info_decrement_strong_count(fn_info) },
+            true
+        );
+        assert_eq!(Arc::strong_count(&fn_info_arc), strong_count - 1);
+
+        mem::forget(fn_info_arc);
+    }
+
+    #[test]
+    fn test_function_info_increment_strong_count_invalid_fn_info() {
+        assert_eq!(
+            unsafe { mun_function_info_increment_strong_count(FunctionInfoHandle::null()) },
+            false
+        );
+    }
+
+    #[test]
+    fn test_function_info_increment_strong_count() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let fn_info = get_fake_function_info(driver.runtime, "main");
+
+        let fn_info_arc = unsafe { Arc::from_raw(fn_info.0 as *const FunctionDefinition) };
+        let strong_count = Arc::strong_count(&fn_info_arc);
+        assert!(strong_count > 0);
+
+        assert_eq!(
+            unsafe { mun_function_info_increment_strong_count(fn_info) },
+            true
+        );
+        assert_eq!(Arc::strong_count(&fn_info_arc), strong_count + 1);
+
+        mem::forget(fn_info_arc);
+    }
+
+    #[test]
+    fn test_function_info_fn_ptr_invalid_fn_info() {
+        let fn_ptr = unsafe { mun_function_info_fn_ptr(FunctionInfoHandle::null()) };
+        assert_eq!(fn_ptr, ptr::null());
+    }
+
+    #[test]
+    fn test_function_info_fn_ptr() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let fn_info = get_fake_function_info(driver.runtime, "main");
+        let fn_ptr = unsafe { mun_function_info_fn_ptr(fn_info) };
+        assert_ne!(fn_ptr, ptr::null());
+    }
+
+    #[test]
+    fn test_function_info_name_invalid_fn_info() {
+        let fn_ptr = unsafe { mun_function_info_name(FunctionInfoHandle::null()) };
+        assert_eq!(fn_ptr, ptr::null());
+    }
+
+    #[test]
+    fn test_function_info_name() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let fn_info = get_fake_function_info(driver.runtime, "main");
+        let name = unsafe { mun_function_info_name(fn_info) };
+        assert_ne!(name, ptr::null());
+
+        let name_str = unsafe { CStr::from_ptr(name) }
+            .to_str()
+            .expect("Invalid function name.");
+
+        assert_eq!(name_str, "main");
+
+        unsafe { mun_string_destroy(name) };
+    }
+
+    #[test]
+    fn test_function_info_argument_types_invalid_fn_info() {
+        let handle = unsafe {
+            mun_function_info_argument_types(FunctionInfoHandle::null(), ptr::null_mut())
+        };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'fn_info' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_function_info_argument_types_invalid_arg_types() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let fn_info = get_fake_function_info(driver.runtime, "main");
+        let handle = unsafe { mun_function_info_argument_types(fn_info, ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'arg_types' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_function_info_argument_types_none() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let fn_info = get_fake_function_info(driver.runtime, "main");
+        let mut arg_types = MaybeUninit::uninit();
+        let handle = unsafe { mun_function_info_argument_types(fn_info, arg_types.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let arg_types = unsafe { arg_types.assume_init() };
+        assert_eq!(arg_types.data, ptr::null());
+        assert_eq!(arg_types.len, 0);
+    }
+
+    #[test]
+    fn test_function_info_argument_types_some() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn add(a: i32, b: i32) -> i32 { a + b }
+    "#,
+        );
+
+        let fn_info = get_fake_function_info(driver.runtime, "add");
+        let mut arg_types = MaybeUninit::uninit();
+        let handle = unsafe { mun_function_info_argument_types(fn_info, arg_types.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let arg_types = unsafe { arg_types.assume_init() };
+        assert_eq!(arg_types.len, 2);
+
+        (0..arg_types.len).into_iter().for_each(|index| {
+            let type_info = arg_types.get(index);
+            assert_ne!(type_info.0, ptr::null());
+
+            let mut type_id = MaybeUninit::uninit();
+            let handle = unsafe { mun_type_info_id(type_info, type_id.as_mut_ptr()) };
+            assert_eq!(handle.0, ptr::null());
+
+            let type_id = unsafe { type_id.assume_init() };
+            assert_eq!(type_id, <i32>::type_info().id);
+        })
+    }
+
+    #[test]
+    fn test_function_info_return_type_invalid_fn_info() {
+        let return_type = unsafe { mun_function_info_return_type(FunctionInfoHandle::null()) };
+        assert_eq!(return_type.0, ptr::null());
+    }
+
+    #[test]
+    fn test_function_info_return_type_none() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() { }
+    "#,
+        );
+
+        let fn_info = get_fake_function_info(driver.runtime, "main");
+        let return_type = unsafe { mun_function_info_return_type(fn_info) };
+        assert_ne!(return_type.0, ptr::null());
+
+        let mut type_id = MaybeUninit::uninit();
+        let handle = unsafe { mun_type_info_id(return_type, type_id.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let type_id = unsafe { type_id.assume_init() };
+        assert_eq!(type_id, <()>::type_info().id);
+    }
+
+    #[test]
+    fn test_function_info_return_type_some() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let fn_info = get_fake_function_info(driver.runtime, "main");
+        let return_type = unsafe { mun_function_info_return_type(fn_info) };
+        assert_ne!(return_type.0, ptr::null());
+
+        let mut type_id = MaybeUninit::uninit();
+        let handle = unsafe { mun_type_info_id(return_type, type_id.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let type_id = unsafe { type_id.assume_init() };
+        assert_eq!(type_id, <i32>::type_info().id);
+    }
+}
diff --git a/crates/mun_runtime_capi/src/gc.rs b/crates/mun_runtime_capi/src/gc.rs
index c758f263a..f6d9bccc2 100644
--- a/crates/mun_runtime_capi/src/gc.rs
+++ b/crates/mun_runtime_capi/src/gc.rs
@@ -1,11 +1,12 @@
 //! Exposes Mun garbage collection.
 
-use crate::{ErrorHandle, RuntimeHandle, HUB};
-use anyhow::anyhow;
+use std::{ffi::c_void, sync::Arc};
+
+use crate::{error::ErrorHandle, runtime::RuntimeHandle, type_info::TypeInfoHandle};
+use memory::TypeInfo;
 use runtime::Runtime;
 
 pub use memory::gc::GcPtr;
-pub use runtime::UnsafeTypeInfo;
 
 /// Allocates an object in the runtime of the given `type_info`. If successful, `obj` is set,
 /// otherwise a non-zero error handle is returned.
@@ -19,29 +20,30 @@ pub use runtime::UnsafeTypeInfo;
 /// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
 #[no_mangle]
 pub unsafe extern "C" fn mun_gc_alloc(
-    handle: RuntimeHandle,
-    type_info: UnsafeTypeInfo,
+    runtime: RuntimeHandle,
+    type_info: TypeInfoHandle,
     obj: *mut GcPtr,
 ) -> ErrorHandle {
-    let runtime = match (handle.0 as *mut Runtime).as_ref() {
+    let runtime = match (runtime.0 as *mut Runtime).as_ref() {
         Some(runtime) => runtime,
-        None => {
-            return HUB
-                .errors
-                .register(anyhow!("Invalid argument: 'runtime' is null pointer."))
-        }
+        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
     };
 
+    if type_info.0.is_null() {
+        return ErrorHandle::new("Invalid argument: 'type_info' is null pointer.");
+    }
+
     let obj = match obj.as_mut() {
         Some(obj) => obj,
-        None => {
-            return HUB
-                .errors
-                .register(anyhow!("Invalid argument: 'obj' is null pointer."))
-        }
+        None => return ErrorHandle::new("Invalid argument: 'obj' is null pointer."),
     };
 
-    *obj = runtime.gc().alloc(type_info);
+    let type_info = Arc::from_raw(type_info.0 as *const TypeInfo);
+
+    *obj = runtime.gc().alloc(&type_info);
+
+    std::mem::forget(type_info);
+
     ErrorHandle::default()
 }
 
@@ -59,27 +61,20 @@ pub unsafe extern "C" fn mun_gc_alloc(
 pub unsafe extern "C" fn mun_gc_ptr_type(
     handle: RuntimeHandle,
     obj: GcPtr,
-    type_info: *mut UnsafeTypeInfo,
+    type_info: *mut TypeInfoHandle,
 ) -> ErrorHandle {
     let runtime = match (handle.0 as *mut Runtime).as_ref() {
         Some(runtime) => runtime,
-        None => {
-            return HUB
-                .errors
-                .register(anyhow!("Invalid argument: 'runtime' is null pointer."))
-        }
+        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
     };
 
     let type_info = match type_info.as_mut() {
         Some(type_info) => type_info,
-        None => {
-            return HUB
-                .errors
-                .register(anyhow!("Invalid argument: 'type_info' is null pointer."))
-        }
+        None => return ErrorHandle::new("Invalid argument: 'type_info' is null pointer."),
     };
 
-    *type_info = runtime.gc().ptr_type(obj);
+    type_info.0 = Arc::into_raw(runtime.gc().ptr_type(obj)) as *const c_void;
+
     ErrorHandle::default()
 }
 
@@ -100,11 +95,7 @@ pub unsafe extern "C" fn mun_gc_ptr_type(
 pub unsafe extern "C" fn mun_gc_root(handle: RuntimeHandle, obj: GcPtr) -> ErrorHandle {
     let runtime = match (handle.0 as *mut Runtime).as_ref() {
         Some(runtime) => runtime,
-        None => {
-            return HUB
-                .errors
-                .register(anyhow!("Invalid argument: 'runtime' is null pointer."))
-        }
+        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
     };
 
     runtime.gc().root(obj);
@@ -127,11 +118,7 @@ pub unsafe extern "C" fn mun_gc_root(handle: RuntimeHandle, obj: GcPtr) -> Error
 pub unsafe extern "C" fn mun_gc_unroot(handle: RuntimeHandle, obj: GcPtr) -> ErrorHandle {
     let runtime = match (handle.0 as *mut Runtime).as_ref() {
         Some(runtime) => runtime,
-        None => {
-            return HUB
-                .errors
-                .register(anyhow!("Invalid argument: 'runtime' is null pointer."))
-        }
+        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
     };
 
     runtime.gc().unroot(obj);
@@ -156,22 +143,258 @@ pub unsafe extern "C" fn mun_gc_collect(
 ) -> ErrorHandle {
     let runtime = match (handle.0 as *mut Runtime).as_ref() {
         Some(runtime) => runtime,
-        None => {
-            return HUB
-                .errors
-                .register(anyhow!("Invalid argument: 'runtime' is null pointer."))
-        }
+        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
     };
 
     let reclaimed = match reclaimed.as_mut() {
         Some(reclaimed) => reclaimed,
-        None => {
-            return HUB
-                .errors
-                .register(anyhow!("Invalid argument: 'reclaimed' is null pointer."))
-        }
+        None => return ErrorHandle::new("Invalid argument: 'reclaimed' is null pointer."),
     };
 
     *reclaimed = runtime.gc_collect();
     ErrorHandle::default()
 }
+
+#[cfg(test)]
+mod tests {
+    use memory::gc::{HasIndirectionPtr, RawGcPtr};
+
+    use super::*;
+    use crate::{
+        error::mun_error_destroy, runtime::mun_runtime_get_type_info_by_name, test_invalid_runtime,
+        test_util::TestDriver,
+    };
+    use std::{
+        ffi::{CStr, CString},
+        mem::{self, MaybeUninit},
+        ptr,
+    };
+
+    test_invalid_runtime!(
+        gc_alloc(TypeInfoHandle::null(), ptr::null_mut()),
+        gc_ptr_type(mem::zeroed::<GcPtr>(), ptr::null_mut()),
+        gc_root(mem::zeroed::<GcPtr>()),
+        gc_unroot(mem::zeroed::<GcPtr>()),
+        gc_collect(ptr::null_mut())
+    );
+
+    #[test]
+    fn test_gc_alloc_invalid_type_info() {
+        let driver = TestDriver::new(
+            r#"
+        pub struct Foo;
+    "#,
+        );
+
+        let handle =
+            unsafe { mun_gc_alloc(driver.runtime, TypeInfoHandle::null(), ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'type_info' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_gc_alloc_invalid_obj() {
+        let driver = TestDriver::new(
+            r#"
+        pub struct Foo;
+    "#,
+        );
+
+        let type_name = CString::new("Foo").expect("Invalid type name.");
+        let mut has_type = false;
+        let mut type_info = TypeInfoHandle::null();
+
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_name(
+                driver.runtime,
+                type_name.as_ptr(),
+                &mut has_type as *mut bool,
+                &mut type_info as *mut TypeInfoHandle,
+            )
+        };
+        assert_eq!(handle.0, ptr::null());
+
+        let handle = unsafe { mun_gc_alloc(driver.runtime, type_info, ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'obj' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_gc_alloc() {
+        let driver = TestDriver::new(
+            r#"
+        pub struct Foo;
+    "#,
+        );
+
+        let type_name = CString::new("Foo").expect("Invalid type name.");
+        let mut has_type = false;
+        let mut type_info = TypeInfoHandle::null();
+
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_name(
+                driver.runtime,
+                type_name.as_ptr(),
+                &mut has_type as *mut bool,
+                &mut type_info as *mut TypeInfoHandle,
+            )
+        };
+        assert_eq!(handle.0, ptr::null());
+
+        let mut obj = MaybeUninit::uninit();
+        let handle = unsafe { mun_gc_alloc(driver.runtime, type_info, obj.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let obj = unsafe { obj.assume_init() };
+        assert_ne!(unsafe { obj.deref::<u8>() }, ptr::null());
+
+        let mut reclaimed = false;
+        let handle = unsafe { mun_gc_collect(driver.runtime, &mut reclaimed as *mut _) };
+        assert_eq!(handle.0, ptr::null());
+    }
+
+    #[test]
+    fn test_gc_ptr_type_invalid_type_info() {
+        let driver = TestDriver::new(
+            r#"
+        pub struct Foo;
+    "#,
+        );
+
+        let handle = unsafe {
+            let raw_ptr: RawGcPtr = ptr::null();
+            mun_gc_ptr_type(driver.runtime, raw_ptr.into(), ptr::null_mut())
+        };
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'type_info' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_gc_ptr_type() {
+        let driver = TestDriver::new(
+            r#"
+        pub struct Foo;
+    "#,
+        );
+
+        let type_name = CString::new("Foo").expect("Invalid type name.");
+        let mut has_type = false;
+        let mut type_info = TypeInfoHandle::null();
+
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_name(
+                driver.runtime,
+                type_name.as_ptr(),
+                &mut has_type as *mut bool,
+                &mut type_info as *mut TypeInfoHandle,
+            )
+        };
+        assert_eq!(handle.0, ptr::null());
+
+        let mut obj = MaybeUninit::uninit();
+        let handle = unsafe { mun_gc_alloc(driver.runtime, type_info, obj.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let obj = unsafe { obj.assume_init() };
+        assert_ne!(unsafe { obj.deref::<u8>() }, ptr::null());
+
+        let mut ty = MaybeUninit::uninit();
+        let handle = unsafe { mun_gc_ptr_type(driver.runtime, obj, ty.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let ty = unsafe { ty.assume_init() };
+        assert_eq!(type_info.0, ty.0);
+
+        let mut reclaimed = false;
+        let handle = unsafe { mun_gc_collect(driver.runtime, &mut reclaimed as *mut _) };
+        assert_eq!(handle.0, ptr::null());
+        assert!(reclaimed);
+    }
+
+    #[test]
+    fn test_gc_rooting() {
+        let driver = TestDriver::new(
+            r#"
+        pub struct Foo;
+    "#,
+        );
+
+        let type_name = CString::new("Foo").expect("Invalid type name.");
+        let mut has_type = false;
+        let mut type_info = TypeInfoHandle::null();
+
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_name(
+                driver.runtime,
+                type_name.as_ptr(),
+                &mut has_type as *mut bool,
+                &mut type_info as *mut TypeInfoHandle,
+            )
+        };
+        assert_eq!(handle.0, ptr::null());
+
+        let mut obj = MaybeUninit::uninit();
+        let handle = unsafe { mun_gc_alloc(driver.runtime, type_info, obj.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let obj = unsafe { obj.assume_init() };
+        assert_ne!(unsafe { obj.deref::<u8>() }, ptr::null());
+
+        let handle = unsafe { mun_gc_root(driver.runtime, obj) };
+
+        assert_eq!(handle.0, ptr::null());
+
+        let mut reclaimed = false;
+        let handle = unsafe { mun_gc_collect(driver.runtime, &mut reclaimed as *mut _) };
+        assert_eq!(handle.0, ptr::null());
+        assert!(!reclaimed);
+
+        let handle = unsafe { mun_gc_unroot(driver.runtime, obj) };
+        assert_eq!(handle.0, ptr::null());
+
+        let handle = unsafe { mun_gc_collect(driver.runtime, &mut reclaimed as *mut _) };
+        assert_eq!(handle.0, ptr::null());
+        assert!(reclaimed);
+    }
+
+    #[test]
+    fn test_gc_ptr_collect_invalid_reclaimed() {
+        let driver = TestDriver::new(
+            r#"
+        pub struct Foo;
+
+        pub fn main() -> Foo { Foo }
+    "#,
+        );
+
+        let handle = unsafe { mun_gc_collect(driver.runtime, ptr::null_mut()) };
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'reclaimed' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+}
diff --git a/crates/mun_runtime_capi/src/hub.rs b/crates/mun_runtime_capi/src/hub.rs
deleted file mode 100644
index 2fe6d7c4f..000000000
--- a/crates/mun_runtime_capi/src/hub.rs
+++ /dev/null
@@ -1,81 +0,0 @@
-//! A statically-allocated, concurrent data structure for storage of Rust objects that are utilized
-//! through Mun Runtime C API calls.
-
-use std::collections::HashMap;
-use std::hash::Hash;
-use std::sync::Arc;
-
-use lazy_static::lazy_static;
-use parking_lot::{RwLock, RwLockReadGuard};
-
-use crate::error::ErrorHandle;
-use crate::TypedHandle;
-
-fn generate_handle<H: TypedHandle>() -> H {
-    loop {
-        let token = rand::random();
-        if token != 0 {
-            return H::new(token);
-        }
-    }
-}
-
-/// A concurrent registry for uniquely indexed values.
-pub struct Registry<H, T> {
-    data: RwLock<HashMap<H, T>>,
-}
-
-impl<H, T> Registry<H, T>
-where
-    H: Copy + Eq + Hash + TypedHandle,
-{
-    /// Inserts `value` and returns a unique handle to it.
-    pub fn register(&self, value: T) -> H {
-        let handle = {
-            let data = self.data.read();
-
-            let mut handle = generate_handle();
-            while data.contains_key(&handle) {
-                handle = generate_handle();
-            }
-            handle
-        };
-
-        self.data.write().insert(handle, value);
-        handle
-    }
-
-    /// Removes and returns the value corresponding to `handle`, if it is found.
-    pub fn unregister(&self, handle: H) -> Option<T> {
-        self.data.write().remove(&handle)
-    }
-
-    /// Retrieves the inner data
-    pub fn get_data(&self) -> RwLockReadGuard<HashMap<H, T>> {
-        self.data.read()
-    }
-}
-
-impl<H, T> Default for Registry<H, T>
-where
-    H: Eq + Hash + TypedHandle,
-{
-    fn default() -> Self {
-        Registry {
-            data: RwLock::new(HashMap::new()),
-        }
-    }
-}
-
-/// Concurrent data structure for storage of Rust objects that are utilized through Mun Runtime
-/// C API calls.
-#[derive(Default)]
-pub struct Hub {
-    /// Error registry
-    pub errors: Arc<Registry<ErrorHandle, anyhow::Error>>,
-}
-
-lazy_static! {
-    /// Storage for Rust objects that are utilized through Mun Runtime C API calls.
-    pub static ref HUB: Hub = Hub::default();
-}
diff --git a/crates/mun_runtime_capi/src/lib.rs b/crates/mun_runtime_capi/src/lib.rs
index d026b47d4..e5bc25b8c 100644
--- a/crates/mun_runtime_capi/src/lib.rs
+++ b/crates/mun_runtime_capi/src/lib.rs
@@ -6,256 +6,18 @@
 
 pub mod error;
 pub mod gc;
-pub mod hub;
+pub mod runtime;
 
-#[cfg(test)]
-mod tests;
-
-use std::{
-    ffi::{c_void, CStr, CString},
-    os::raw::c_char,
-};
-
-use crate::{error::ErrorHandle, hub::HUB};
-use anyhow::anyhow;
-use runtime::Runtime;
-
-pub(crate) type Token = usize;
-
-pub use memory::gc::GcPtr;
-
-/// A type to uniquely index typed collections.
-pub trait TypedHandle {
-    /// Constructs a new `TypedHandle`.
-    fn new(token: Token) -> Self;
-    /// Retrieves the handle's token.
-    fn token(&self) -> Token;
-}
-
-/// A C-style handle to a runtime.
-#[repr(C)]
-#[derive(Clone, Copy)]
-pub struct RuntimeHandle(*mut c_void);
-
-/// Options required to construct a [`RuntimeHandle`] through [`mun_runtime_create`]
-///
-/// # Safety
-///
-/// This struct contains raw pointers as parameters. Passing pointers to invalid data, will lead to
-/// undefined behavior.
-#[repr(C)]
-#[derive(Clone, Copy)]
-pub struct RuntimeOptions {
-    /// Function definitions that should be inserted in the runtime before a mun library is loaded.
-    /// This is useful to initialize `extern` functions used in a mun library.
-    ///
-    /// If the [`num_functions`] fields is non-zero this field must contain a pointer to an array
-    /// of [`abi::FunctionDefinition`]s.
-    pub functions: *const abi::FunctionDefinition,
-
-    /// The number of functions in the [`functions`] array.
-    pub num_functions: u32,
-}
-
-impl Default for RuntimeOptions {
-    fn default() -> Self {
-        RuntimeOptions {
-            functions: std::ptr::null(),
-            num_functions: 0,
-        }
-    }
-}
-
-/// Constructs a new runtime that loads the library at `library_path` and its dependencies. If
-/// successful, the runtime `handle` is set, otherwise a non-zero error handle is returned.
-///
-/// If a non-zero error handle is returned, it must be manually destructed using
-/// [`mun_error_destroy`].
-///
-/// The runtime must be manually destructed using [`mun_runtime_destroy`].
-///
-/// # Safety
-///
-/// This function receives raw pointers as parameters. If any of the arguments is a null pointer,
-/// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
-#[no_mangle]
-pub unsafe extern "C" fn mun_runtime_create(
-    library_path: *const c_char,
-    options: RuntimeOptions,
-    handle: *mut RuntimeHandle,
-) -> ErrorHandle {
-    if library_path.is_null() {
-        return HUB
-            .errors
-            .register(anyhow!("Invalid argument: 'library_path' is null pointer."));
-    }
-
-    if options.num_functions > 0 && options.functions.is_null() {
-        return HUB
-            .errors
-            .register(anyhow!("Invalid argument: 'functions' is null pointer."));
-    }
-
-    let library_path = match CStr::from_ptr(library_path).to_str() {
-        Ok(path) => path,
-        Err(_) => {
-            return HUB.errors.register(anyhow!(
-                "Invalid argument: 'library_path' is not UTF-8 encoded.",
-            ))
-        }
-    };
-
-    let handle = match handle.as_mut() {
-        Some(handle) => handle,
-        None => {
-            return HUB
-                .errors
-                .register(anyhow!("Invalid argument: 'handle' is null pointer."))
-        }
-    };
-
-    let user_functions =
-        std::slice::from_raw_parts(options.functions, options.num_functions as usize)
-            .iter()
-            .map(|def| {
-                abi::FunctionDefinitionStorage::new_function(
-                    def.prototype.name(),
-                    def.prototype.signature.arg_types(),
-                    def.prototype.signature.return_type(),
-                    def.fn_ptr,
-                )
-            })
-            .collect();
-
-    let runtime_options = runtime::RuntimeOptions {
-        library_path: library_path.into(),
-        user_functions,
-    };
+pub mod field_info;
+pub mod function_info;
+pub mod struct_info;
+pub mod type_info;
 
-    let runtime = match Runtime::new(runtime_options) {
-        Ok(runtime) => runtime,
-        Err(e) => return HUB.errors.register(e),
-    };
-
-    handle.0 = Box::into_raw(Box::new(runtime)) as *mut _;
-    ErrorHandle::default()
-}
-
-/// Destructs the runtime corresponding to `handle`.
-#[no_mangle]
-pub extern "C" fn mun_runtime_destroy(handle: RuntimeHandle) {
-    if !handle.0.is_null() {
-        let _runtime = unsafe { Box::from_raw(handle.0) };
-    }
-}
-
-/// Retrieves the [`FunctionDefinition`] for `fn_name` from the runtime corresponding to `handle`.
-/// If successful, `has_fn_info` and `fn_info` are set, otherwise a non-zero error handle is
-/// returned.
-///
-/// If a non-zero error handle is returned, it must be manually destructed using
-/// [`mun_error_destroy`].
-///
-/// # Safety
-///
-/// This function receives raw pointers as parameters. If any of the arguments is a null pointer,
-/// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
-#[no_mangle]
-pub unsafe extern "C" fn mun_runtime_get_function_definition(
-    handle: RuntimeHandle,
-    fn_name: *const c_char,
-    has_fn_info: *mut bool,
-    fn_definition: *mut abi::FunctionDefinition,
-) -> ErrorHandle {
-    let runtime = match (handle.0 as *mut Runtime).as_ref() {
-        Some(runtime) => runtime,
-        None => {
-            return HUB
-                .errors
-                .register(anyhow!("Invalid argument: 'runtime' is null pointer."))
-        }
-    };
-
-    if fn_name.is_null() {
-        return HUB
-            .errors
-            .register(anyhow!("Invalid argument: 'fn_name' is null pointer."));
-    }
-
-    let fn_name = match CStr::from_ptr(fn_name).to_str() {
-        Ok(name) => name,
-        Err(_) => {
-            return HUB
-                .errors
-                .register(anyhow!("Invalid argument: 'fn_name' is not UTF-8 encoded."))
-        }
-    };
-
-    let has_fn_info = match has_fn_info.as_mut() {
-        Some(has_info) => has_info,
-        None => {
-            return HUB
-                .errors
-                .register(anyhow!("Invalid argument: 'has_fn_info' is null pointer."))
-        }
-    };
-
-    let fn_definition = match fn_definition.as_mut() {
-        Some(info) => info,
-        None => {
-            return HUB
-                .errors
-                .register(anyhow!("Invalid argument: 'fn_info' is null pointer."))
-        }
-    };
-
-    match runtime.get_function_definition(fn_name) {
-        Some(info) => {
-            *has_fn_info = true;
-            *fn_definition = info.clone();
-        }
-        None => *has_fn_info = false,
-    }
-
-    ErrorHandle::default()
-}
-
-/// Updates the runtime corresponding to `handle`. If successful, `updated` is set, otherwise a
-/// non-zero error handle is returned.
-///
-/// If a non-zero error handle is returned, it must be manually destructed using
-/// [`mun_error_destroy`].
-///
-/// # Safety
-///
-/// This function receives raw pointers as parameters. If any of the arguments is a null pointer,
-/// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
-#[no_mangle]
-pub unsafe extern "C" fn mun_runtime_update(
-    handle: RuntimeHandle,
-    updated: *mut bool,
-) -> ErrorHandle {
-    let runtime = match (handle.0 as *mut Runtime).as_mut() {
-        Some(runtime) => runtime,
-        None => {
-            return HUB
-                .errors
-                .register(anyhow!("Invalid argument: 'runtime' is null pointer."))
-        }
-    };
-
-    let updated = match updated.as_mut() {
-        Some(updated) => updated,
-        None => {
-            return HUB
-                .errors
-                .register(anyhow!("Invalid argument: 'updated' is null pointer."))
-        }
-    };
+#[macro_use]
+#[cfg(test)]
+mod test_util;
 
-    *updated = runtime.update();
-    ErrorHandle::default()
-}
+use std::{ffi::CString, os::raw::c_char};
 
 /// Deallocates a string that was allocated by the runtime.
 ///
@@ -265,7 +27,7 @@ pub unsafe extern "C" fn mun_runtime_update(
 /// its content will be deallocated. Passing pointers to invalid data or memory allocated by other
 /// processes, will lead to undefined behavior.
 #[no_mangle]
-pub unsafe extern "C" fn mun_destroy_string(string: *const c_char) {
+pub unsafe extern "C" fn mun_string_destroy(string: *const c_char) {
     if !string.is_null() {
         // Destroy the string
         let _string = CString::from_raw(string as *mut _);
diff --git a/crates/mun_runtime_capi/src/runtime.rs b/crates/mun_runtime_capi/src/runtime.rs
new file mode 100644
index 000000000..12cdefdad
--- /dev/null
+++ b/crates/mun_runtime_capi/src/runtime.rs
@@ -0,0 +1,932 @@
+//! Exposes the Mun runtime using the C ABI.
+
+use memory::type_table::TypeTable;
+use runtime::{FunctionDefinition, Runtime};
+use std::{
+    ffi::{c_void, CStr},
+    os::raw::c_char,
+    sync::Arc,
+};
+
+use crate::{error::ErrorHandle, function_info::FunctionInfoHandle, type_info::TypeInfoHandle};
+
+/// A C-style handle to a runtime.
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RuntimeHandle(pub *mut c_void);
+
+/// Options required to construct a [`RuntimeHandle`] through [`mun_runtime_create`]
+///
+/// # Safety
+///
+/// This struct contains raw pointers as parameters. Passing pointers to invalid data, will lead to
+/// undefined behavior.
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RuntimeOptions {
+    /// Function definitions that should be inserted in the runtime before a mun library is loaded.
+    /// This is useful to initialize `extern` functions used in a mun library.
+    ///
+    /// If the [`num_functions`] fields is non-zero this field must contain a pointer to an array
+    /// of [`abi::FunctionDefinition`]s.
+    pub functions: *const abi::FunctionDefinition,
+
+    /// The number of functions in the [`functions`] array.
+    pub num_functions: u32,
+}
+
+impl Default for RuntimeOptions {
+    fn default() -> Self {
+        RuntimeOptions {
+            functions: std::ptr::null(),
+            num_functions: 0,
+        }
+    }
+}
+
+/// Constructs a new runtime that loads the library at `library_path` and its dependencies. If
+/// successful, the runtime `handle` is set, otherwise a non-zero error handle is returned.
+///
+/// If a non-zero error handle is returned, it must be manually destructed using
+/// [`mun_error_destroy`].
+///
+/// The runtime must be manually destructed using [`mun_runtime_destroy`].
+///
+/// # Safety
+///
+/// This function receives raw pointers as parameters. If any of the arguments is a null pointer,
+/// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
+#[no_mangle]
+pub unsafe extern "C" fn mun_runtime_create(
+    library_path: *const c_char,
+    options: RuntimeOptions,
+    handle: *mut RuntimeHandle,
+) -> ErrorHandle {
+    if library_path.is_null() {
+        return ErrorHandle::new("Invalid argument: 'library_path' is null pointer.");
+    }
+
+    if options.num_functions > 0 && options.functions.is_null() {
+        return ErrorHandle::new("Invalid argument: 'functions' is null pointer.");
+    }
+
+    let library_path = match CStr::from_ptr(library_path).to_str() {
+        Ok(path) => path,
+        Err(_) => {
+            return ErrorHandle::new("Invalid argument: 'library_path' is not UTF-8 encoded.");
+        }
+    };
+
+    let handle = match handle.as_mut() {
+        Some(handle) => handle,
+        None => return ErrorHandle::new("Invalid argument: 'handle' is null pointer."),
+    };
+
+    let type_table = TypeTable::default();
+    let user_functions =
+        match std::slice::from_raw_parts(options.functions, options.num_functions as usize)
+            .iter()
+            .map(|def| FunctionDefinition::try_from_abi(def, &type_table))
+            .collect::<Result<_, _>>()
+        {
+            Err(e) => return ErrorHandle::new(e.to_string()),
+            Ok(user_functions) => user_functions,
+        };
+
+    let runtime_options = runtime::RuntimeOptions {
+        library_path: library_path.into(),
+        user_functions,
+        type_table,
+    };
+
+    let runtime = match Runtime::new(runtime_options) {
+        Ok(runtime) => runtime,
+        Err(e) => return ErrorHandle::new(e.to_string()),
+    };
+
+    handle.0 = Box::into_raw(Box::new(runtime)) as *mut _;
+    ErrorHandle::default()
+}
+
+/// Destructs the runtime corresponding to `handle`.
+#[no_mangle]
+pub extern "C" fn mun_runtime_destroy(handle: RuntimeHandle) {
+    if !handle.0.is_null() {
+        let _runtime = unsafe { Box::from_raw(handle.0) };
+    }
+}
+
+/// Retrieves the [`FunctionDefinition`] for `fn_name` from the runtime corresponding to `handle`.
+/// If successful, `has_fn_info` and `fn_info` are set, otherwise a non-zero error handle is
+/// returned.
+///
+/// If a non-zero error handle is returned, it must be manually destructed using
+/// [`mun_error_destroy`].
+///
+/// # Safety
+///
+/// This function receives raw pointers as parameters. If any of the arguments is a null pointer,
+/// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
+#[no_mangle]
+pub unsafe extern "C" fn mun_runtime_get_function_info(
+    handle: RuntimeHandle,
+    fn_name: *const c_char,
+    has_fn_info: *mut bool,
+    fn_info: *mut FunctionInfoHandle,
+) -> ErrorHandle {
+    let runtime = match (handle.0 as *mut Runtime).as_ref() {
+        Some(runtime) => runtime,
+        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
+    };
+
+    if fn_name.is_null() {
+        return ErrorHandle::new("Invalid argument: 'fn_name' is null pointer.");
+    }
+
+    let fn_name = match CStr::from_ptr(fn_name).to_str() {
+        Ok(name) => name,
+        Err(_) => {
+            return ErrorHandle::new("Invalid argument: 'fn_name' is not UTF-8 encoded.");
+        }
+    };
+
+    let has_fn_info = match has_fn_info.as_mut() {
+        Some(has_info) => has_info,
+        None => return ErrorHandle::new("Invalid argument: 'has_fn_info' is null pointer."),
+    };
+
+    let fn_info = match fn_info.as_mut() {
+        Some(fn_info) => fn_info,
+        None => return ErrorHandle::new("Invalid argument: 'fn_info' is null pointer."),
+    };
+
+    match runtime.get_function_definition(fn_name) {
+        Some(info) => {
+            *has_fn_info = true;
+            fn_info.0 = Arc::into_raw(info) as *const c_void;
+        }
+        None => *has_fn_info = false,
+    }
+
+    ErrorHandle::default()
+}
+
+/// Retrieves the type information corresponding to the specified `type_name` from the runtime.
+/// If successful, `has_type_info` and `type_info` are set, otherwise a non-zero error handle is
+/// returned.
+///
+/// If a non-zero error handle is returned, it must be manually destructed using
+/// [`mun_error_destroy`].
+///
+/// # Safety
+///
+/// This function receives raw pointers as parameters. If any of the arguments is a null pointer,
+/// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
+#[no_mangle]
+pub unsafe extern "C" fn mun_runtime_get_type_info_by_name(
+    runtime: RuntimeHandle,
+    type_name: *const c_char,
+    has_type_info: *mut bool,
+    type_info: *mut TypeInfoHandle,
+) -> ErrorHandle {
+    let runtime = match (runtime.0 as *mut Runtime).as_ref() {
+        Some(runtime) => runtime,
+        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
+    };
+
+    if type_name.is_null() {
+        return ErrorHandle::new("Invalid argument: 'type_name' is null pointer.");
+    }
+
+    let type_name = match CStr::from_ptr(type_name).to_str() {
+        Ok(name) => name,
+        Err(_) => return ErrorHandle::new("Invalid argument: 'type_name' is not UTF-8 encoded."),
+    };
+
+    let has_type_info = match has_type_info.as_mut() {
+        Some(has_type) => has_type,
+        None => return ErrorHandle::new("Invalid argument: 'has_type_info' is null pointer."),
+    };
+
+    let type_info = match type_info.as_mut() {
+        Some(type_info) => type_info,
+        None => return ErrorHandle::new("Invalid argument: 'type_info' is null pointer."),
+    };
+
+    match runtime.get_type_info_by_name(type_name) {
+        Some(info) => {
+            *has_type_info = true;
+            type_info.0 = Arc::into_raw(info) as *const c_void;
+        }
+        None => *has_type_info = false,
+    }
+
+    ErrorHandle::default()
+}
+
+/// Retrieves the type information corresponding to the specified `type_id` from the runtime. If
+/// successful, `has_type_info` and `type_info` are set, otherwise a non-zero error handle is
+/// returned.
+///
+/// If a non-zero error handle is returned, it must be manually destructed using
+/// [`mun_error_destroy`].
+///
+/// # Safety
+///
+/// This function receives raw pointers as parameters. If any of the arguments is a null pointer,
+/// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
+#[no_mangle]
+pub unsafe extern "C" fn mun_runtime_get_type_info_by_id(
+    runtime: RuntimeHandle,
+    type_id: *const abi::TypeId,
+    has_type_info: *mut bool,
+    type_info: *mut TypeInfoHandle,
+) -> ErrorHandle {
+    let runtime = match (runtime.0 as *mut Runtime).as_ref() {
+        Some(runtime) => runtime,
+        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
+    };
+
+    let type_id = match type_id.as_ref() {
+        Some(type_id) => type_id,
+        None => {
+            return ErrorHandle::new("Invalid argument: 'type_id' is null pointer.");
+        }
+    };
+
+    let has_type_info = match has_type_info.as_mut() {
+        Some(has_type) => has_type,
+        None => return ErrorHandle::new("Invalid argument: 'has_type_info' is null pointer."),
+    };
+
+    let type_info = match type_info.as_mut() {
+        Some(type_info) => type_info,
+        None => return ErrorHandle::new("Invalid argument: 'type_info' is null pointer."),
+    };
+
+    match runtime.get_type_info_by_id(type_id) {
+        Some(info) => {
+            *has_type_info = true;
+            type_info.0 = Arc::into_raw(info) as *const c_void;
+        }
+        None => *has_type_info = false,
+    }
+
+    ErrorHandle::default()
+}
+
+/// Updates the runtime corresponding to `handle`. If successful, `updated` is set, otherwise a
+/// non-zero error handle is returned.
+///
+/// If a non-zero error handle is returned, it must be manually destructed using
+/// [`mun_error_destroy`].
+///
+/// # Safety
+///
+/// This function receives raw pointers as parameters. If any of the arguments is a null pointer,
+/// an error will be returned. Passing pointers to invalid data, will lead to undefined behavior.
+#[no_mangle]
+pub unsafe extern "C" fn mun_runtime_update(
+    handle: RuntimeHandle,
+    updated: *mut bool,
+) -> ErrorHandle {
+    let runtime = match (handle.0 as *mut Runtime).as_mut() {
+        Some(runtime) => runtime,
+        None => return ErrorHandle::new("Invalid argument: 'runtime' is null pointer."),
+    };
+
+    let updated = match updated.as_mut() {
+        Some(updated) => updated,
+        None => return ErrorHandle::new("Invalid argument: 'updated' is null pointer."),
+    };
+
+    *updated = runtime.update();
+    ErrorHandle::default()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        error::mun_error_destroy, test_invalid_runtime, test_util::TestDriver,
+        type_info::mun_type_info_id,
+    };
+    use std::{ffi::CString, mem::MaybeUninit, ptr};
+
+    test_invalid_runtime!(
+        runtime_get_function_info(ptr::null(), ptr::null_mut(), ptr::null_mut()),
+        runtime_get_type_info_by_name(ptr::null(), ptr::null_mut(), ptr::null_mut()),
+        runtime_get_type_info_by_id(ptr::null(), ptr::null_mut(), ptr::null_mut()),
+        runtime_update(ptr::null_mut())
+    );
+
+    #[test]
+    fn test_runtime_create_invalid_lib_path() {
+        let handle =
+            unsafe { mun_runtime_create(ptr::null(), RuntimeOptions::default(), ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'library_path' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_create_invalid_lib_path_encoding() {
+        let invalid_encoding = ['�', '\0'];
+
+        let handle = unsafe {
+            mun_runtime_create(
+                invalid_encoding.as_ptr() as *const _,
+                RuntimeOptions::default(),
+                ptr::null_mut(),
+            )
+        };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'library_path' is not UTF-8 encoded."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_create_invalid_functions() {
+        let lib_path = CString::new("some/path").expect("Invalid library path");
+
+        let mut options = RuntimeOptions::default();
+        options.num_functions = 1;
+
+        let handle = unsafe { mun_runtime_create(lib_path.into_raw(), options, ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'functions' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_create_invalid_handle() {
+        let lib_path = CString::new("some/path").expect("Invalid library path");
+
+        let handle = unsafe {
+            mun_runtime_create(
+                lib_path.into_raw(),
+                RuntimeOptions::default(),
+                ptr::null_mut(),
+            )
+        };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'handle' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_create_invalid_user_function() {
+        let lib_path = CString::new("some/path").expect("Invalid library path");
+
+        let type_id = abi::TypeId {
+            guid: abi::Guid([0u8; 16]),
+        };
+        let functions = vec![abi::FunctionDefinition {
+            prototype: abi::FunctionPrototype {
+                name: ptr::null(),
+                signature: abi::FunctionSignature {
+                    arg_types: ptr::null(),
+                    return_type: type_id.clone(),
+                    num_arg_types: 0,
+                },
+            },
+            fn_ptr: ptr::null(),
+        }];
+
+        let mut options = RuntimeOptions::default();
+        options.functions = functions.as_ptr();
+        options.num_functions = 1;
+
+        let mut runtime = MaybeUninit::uninit();
+        let handle =
+            unsafe { mun_runtime_create(lib_path.into_raw(), options, runtime.as_mut_ptr()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            format!("unknown TypeId '{}'", type_id)
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_get_function_info_invalid_fn_name() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 3 }
+    "#,
+        );
+
+        let handle = unsafe {
+            mun_runtime_get_function_info(
+                driver.runtime,
+                ptr::null(),
+                ptr::null_mut(),
+                ptr::null_mut(),
+            )
+        };
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'fn_name' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_get_function_info_invalid_fn_name_encoding() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 3 }
+    "#,
+        );
+
+        let invalid_encoding = ['�', '\0'];
+        let handle = unsafe {
+            mun_runtime_get_function_info(
+                driver.runtime,
+                invalid_encoding.as_ptr() as *const _,
+                ptr::null_mut(),
+                ptr::null_mut(),
+            )
+        };
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'fn_name' is not UTF-8 encoded."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_get_function_info_invalid_has_fn_info() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 3 }
+    "#,
+        );
+
+        let fn_name = CString::new("main").expect("Invalid function name");
+        let handle = unsafe {
+            mun_runtime_get_function_info(
+                driver.runtime,
+                fn_name.as_ptr(),
+                ptr::null_mut(),
+                ptr::null_mut(),
+            )
+        };
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'has_fn_info' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_get_function_info_invalid_fn_info() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 3 }
+    "#,
+        );
+
+        let fn_name = CString::new("main").expect("Invalid function name");
+        let mut has_fn_info = false;
+        let handle = unsafe {
+            mun_runtime_get_function_info(
+                driver.runtime,
+                fn_name.as_ptr(),
+                &mut has_fn_info as *mut _,
+                ptr::null_mut(),
+            )
+        };
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'fn_info' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_get_function_info_none() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 3 }
+    "#,
+        );
+
+        let fn_name = CString::new("add").expect("Invalid function name");
+        let mut has_fn_info = false;
+        let mut fn_definition = MaybeUninit::uninit();
+        let handle = unsafe {
+            mun_runtime_get_function_info(
+                driver.runtime,
+                fn_name.as_ptr(),
+                &mut has_fn_info as *mut _,
+                fn_definition.as_mut_ptr(),
+            )
+        };
+        assert_eq!(handle.0, ptr::null());
+        assert!(!has_fn_info);
+    }
+
+    #[test]
+    fn test_runtime_get_function_info_some() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 3 }
+    "#,
+        );
+
+        let fn_name = CString::new("main").expect("Invalid function name");
+        let mut has_fn_info = false;
+        let mut fn_definition = MaybeUninit::uninit();
+        let handle = unsafe {
+            mun_runtime_get_function_info(
+                driver.runtime,
+                fn_name.as_ptr(),
+                &mut has_fn_info as *mut _,
+                fn_definition.as_mut_ptr(),
+            )
+        };
+        assert_eq!(handle.0, ptr::null());
+        assert!(has_fn_info);
+        let _fn_definition = unsafe { fn_definition.assume_init() };
+    }
+
+    #[test]
+    fn test_runtime_get_type_info_by_name_invalid_type_name() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo;
+    "#,
+        );
+
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_name(
+                driver.runtime,
+                ptr::null(),
+                ptr::null_mut(),
+                ptr::null_mut(),
+            )
+        };
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'type_name' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_get_type_info_by_name_invalid_type_name_encoding() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo;
+    "#,
+        );
+
+        let invalid_encoding = ['�', '\0'];
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_name(
+                driver.runtime,
+                invalid_encoding.as_ptr() as *const _,
+                ptr::null_mut(),
+                ptr::null_mut(),
+            )
+        };
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'type_name' is not UTF-8 encoded."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_get_type_info_by_name_invalid_has_type_info() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo;
+    "#,
+        );
+
+        let type_name = CString::new("Foo").expect("Invalid type name");
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_name(
+                driver.runtime,
+                type_name.as_ptr(),
+                ptr::null_mut(),
+                ptr::null_mut(),
+            )
+        };
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'has_type_info' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_get_type_info_by_name_invalid_type_info() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo;
+    "#,
+        );
+
+        let type_name = CString::new("Foo").expect("Invalid type name");
+        let mut has_type_info = false;
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_name(
+                driver.runtime,
+                type_name.as_ptr(),
+                &mut has_type_info as *mut _,
+                ptr::null_mut(),
+            )
+        };
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'type_info' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_get_type_info_by_name_none() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo;
+    "#,
+        );
+
+        let type_name = CString::new("Bar").expect("Invalid type name");
+        let mut has_type_info = false;
+        let mut type_info = MaybeUninit::uninit();
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_name(
+                driver.runtime,
+                type_name.as_ptr(),
+                &mut has_type_info as *mut _,
+                type_info.as_mut_ptr(),
+            )
+        };
+        assert_eq!(handle.0, ptr::null());
+        assert!(!has_type_info);
+    }
+
+    #[test]
+    fn test_runtime_get_type_info_by_name_some() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo;
+    "#,
+        );
+
+        let type_name = CString::new("Foo").expect("Invalid type name");
+        let mut has_type_info = false;
+        let mut type_info = MaybeUninit::uninit();
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_name(
+                driver.runtime,
+                type_name.as_ptr(),
+                &mut has_type_info as *mut _,
+                type_info.as_mut_ptr(),
+            )
+        };
+        assert_eq!(handle.0, ptr::null());
+        assert!(has_type_info);
+        let _type_info = unsafe { type_info.assume_init() };
+    }
+
+    #[test]
+    fn test_runtime_get_type_info_by_id_invalid_type_id() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo;
+    "#,
+        );
+
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_id(
+                driver.runtime,
+                ptr::null(),
+                ptr::null_mut(),
+                ptr::null_mut(),
+            )
+        };
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'type_id' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_get_type_info_by_id_invalid_has_type_info() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo;
+    "#,
+        );
+
+        let type_id = abi::TypeId {
+            guid: abi::Guid([0; 16]),
+        };
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_id(
+                driver.runtime,
+                &type_id as *const abi::TypeId,
+                ptr::null_mut(),
+                ptr::null_mut(),
+            )
+        };
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'has_type_info' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_get_type_info_by_id_invalid_type_info() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo;
+    "#,
+        );
+
+        let type_id = abi::TypeId {
+            guid: abi::Guid([0; 16]),
+        };
+        let mut has_type_info = false;
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_id(
+                driver.runtime,
+                &type_id as *const abi::TypeId,
+                &mut has_type_info as *mut _,
+                ptr::null_mut(),
+            )
+        };
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'type_info' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_get_type_info_by_id_none() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo;
+    "#,
+        );
+
+        let type_id = abi::TypeId {
+            guid: abi::Guid([0u8; 16]),
+        };
+        let mut has_type_info = false;
+        let mut type_info = MaybeUninit::uninit();
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_id(
+                driver.runtime,
+                &type_id as *const abi::TypeId,
+                &mut has_type_info as *mut _,
+                type_info.as_mut_ptr(),
+            )
+        };
+        assert_eq!(handle.0, ptr::null());
+        assert!(!has_type_info);
+    }
+
+    #[test]
+    fn test_runtime_get_type_info_by_id_some() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo;
+    "#,
+        );
+
+        let type_name = CString::new("Foo").expect("Invalid type name");
+        let mut has_type_info = false;
+        let mut type_info = MaybeUninit::uninit();
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_name(
+                driver.runtime,
+                type_name.as_ptr(),
+                &mut has_type_info as *mut _,
+                type_info.as_mut_ptr(),
+            )
+        };
+        assert_eq!(handle.0, ptr::null());
+        assert!(has_type_info);
+        let type_info = unsafe { type_info.assume_init() };
+
+        let mut type_id = MaybeUninit::uninit();
+        let handle = unsafe { mun_type_info_id(type_info, type_id.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let type_id = unsafe { type_id.assume_init() };
+        let mut has_type_info = false;
+        let mut type_info = MaybeUninit::uninit();
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_id(
+                driver.runtime,
+                &type_id as *const abi::TypeId,
+                &mut has_type_info as *mut _,
+                type_info.as_mut_ptr(),
+            )
+        };
+        assert_eq!(handle.0, ptr::null());
+        assert!(has_type_info);
+        let _type_info = unsafe { type_info.assume_init() };
+    }
+
+    #[test]
+    fn test_runtime_update_invalid_updated() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 3 }
+    "#,
+        );
+
+        let handle = unsafe { mun_runtime_update(driver.runtime, ptr::null_mut()) };
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'updated' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_runtime_update() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 3 }
+    "#,
+        );
+
+        let mut updated = false;
+        let handle = unsafe { mun_runtime_update(driver.runtime, &mut updated as *mut _) };
+        assert_eq!(handle.0, ptr::null());
+    }
+}
diff --git a/crates/mun_runtime_capi/src/struct_info.rs b/crates/mun_runtime_capi/src/struct_info.rs
new file mode 100644
index 000000000..5879ef693
--- /dev/null
+++ b/crates/mun_runtime_capi/src/struct_info.rs
@@ -0,0 +1,274 @@
+//! Exposes struct information using the C ABI.
+
+use crate::{
+    error::ErrorHandle,
+    field_info::{FieldInfoHandle, FieldInfoSpan},
+};
+use memory::{FieldInfo, StructInfo};
+use std::{ffi::c_void, mem, ptr};
+
+/// A C-style handle to a `StructInfo`.
+#[repr(C)]
+#[derive(Clone, Copy, Debug)]
+pub struct StructInfoHandle(pub *const c_void);
+
+impl StructInfoHandle {
+    /// A null handle.
+    pub fn null() -> Self {
+        Self(ptr::null())
+    }
+}
+
+/// Retrieves information about the struct's fields.
+///
+/// # Safety
+///
+/// The caller is responsible for calling `mun_field_info_span_destroy` on the returned span.
+///
+/// This function might result in undefined behavior if the [`crate::TypeInfoHandle`] associated
+/// with this `StructInfoHandle` has been deallocated.
+#[no_mangle]
+pub unsafe extern "C" fn mun_struct_info_fields(
+    struct_info: StructInfoHandle,
+    field_info_span: *mut FieldInfoSpan,
+) -> ErrorHandle {
+    let struct_info = match (struct_info.0 as *const StructInfo).as_ref() {
+        Some(struct_info) => struct_info,
+        None => return ErrorHandle::new("Invalid argument: 'struct_info' is null pointer."),
+    };
+
+    let field_info_span = match field_info_span.as_mut() {
+        Some(field_info_span) => field_info_span,
+        None => return ErrorHandle::new("Invalid argument: 'field_info_span' is null pointer."),
+    };
+
+    let mut fields: Vec<FieldInfoHandle> = struct_info
+        .fields
+        .iter()
+        .map(|field| FieldInfoHandle(field as *const FieldInfo as *const c_void))
+        .collect();
+
+    field_info_span.len = fields.len();
+    field_info_span.data = if fields.is_empty() {
+        ptr::null()
+    } else {
+        fields.shrink_to_fit();
+        fields.as_ptr() as *const _
+    };
+
+    // Ownership is transferred
+    mem::forget(fields);
+
+    ErrorHandle::default()
+}
+
+/// Retrieves the struct's memory kind.
+///
+/// # Safety
+///
+/// This function might result in undefined behavior if the [`crate::TypeInfoHandle`] associated
+/// with this `StructInfoHandle` has been deallocated.
+#[no_mangle]
+pub unsafe extern "C" fn mun_struct_info_memory_kind(
+    struct_info: StructInfoHandle,
+    memory_kind: *mut abi::StructMemoryKind,
+) -> ErrorHandle {
+    let struct_info = match (struct_info.0 as *const StructInfo).as_ref() {
+        Some(struct_info) => struct_info,
+        None => return ErrorHandle::new("Invalid argument: 'struct_info' is null pointer."),
+    };
+
+    let memory_kind = match memory_kind.as_mut() {
+        Some(memory_kind) => memory_kind,
+        None => return ErrorHandle::new("Invalid argument: 'memory_kind' is null pointer."),
+    };
+
+    *memory_kind = struct_info.memory_kind;
+
+    ErrorHandle::default()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        error::mun_error_destroy,
+        field_info::{mun_field_info_name, mun_field_info_span_destroy},
+        runtime::RuntimeHandle,
+        test_util::TestDriver,
+        type_info::{mun_type_info_data, tests::get_type_info_by_name},
+    };
+    use std::{ffi::CStr, mem::MaybeUninit, ptr, slice};
+
+    fn get_struct_info_by_name<T: Into<Vec<u8>>>(
+        runtime: RuntimeHandle,
+        type_name: T,
+    ) -> StructInfoHandle {
+        let type_info = get_type_info_by_name(runtime, type_name);
+
+        let mut type_info_data = MaybeUninit::uninit();
+        let handle = unsafe { mun_type_info_data(type_info, type_info_data.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let type_info_data = unsafe { type_info_data.assume_init() };
+        type_info_data
+            .as_struct()
+            .expect("the type name does not represent a struct")
+    }
+
+    #[test]
+    fn test_struct_info_fields_invalid_struct_info() {
+        let handle = unsafe { mun_struct_info_fields(StructInfoHandle::null(), ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'struct_info' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_struct_info_fields_invalid_arg() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo;"#,
+        );
+
+        let foo_struct_info = get_struct_info_by_name(driver.runtime, "Foo");
+        let handle = unsafe { mun_struct_info_fields(foo_struct_info, ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'field_info_span' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_struct_info_fields_none() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo;"#,
+        );
+
+        let foo_struct_info = get_struct_info_by_name(driver.runtime, "Foo");
+
+        let mut foo_fields_span = MaybeUninit::uninit();
+        let handle =
+            unsafe { mun_struct_info_fields(foo_struct_info, foo_fields_span.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let foo_fields_span = unsafe { foo_fields_span.assume_init() };
+        assert_eq!(foo_fields_span.data, ptr::null());
+        assert_eq!(foo_fields_span.len, 0);
+    }
+
+    #[test]
+    fn test_struct_info_fields_some() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo {
+                bar: i8,
+                baz: f32,
+            }"#,
+        );
+
+        let foo_struct_info = get_struct_info_by_name(driver.runtime, "Foo");
+
+        let mut foo_fields_span = MaybeUninit::uninit();
+        let handle =
+            unsafe { mun_struct_info_fields(foo_struct_info, foo_fields_span.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let foo_fields_span = unsafe { foo_fields_span.assume_init() };
+        assert_eq!(foo_fields_span.len, 2);
+
+        let foo_fields: Vec<_> =
+            unsafe { slice::from_raw_parts(foo_fields_span.data, foo_fields_span.len) }
+                .iter()
+                .map(|field_info| {
+                    unsafe { CStr::from_ptr(mun_field_info_name(*field_info)) }
+                        .to_str()
+                        .expect("invalid field name")
+                        .to_owned()
+                })
+                .collect();
+
+        assert!(unsafe { mun_field_info_span_destroy(foo_fields_span) });
+
+        assert_eq!(vec![String::from("bar"), String::from("baz")], foo_fields);
+    }
+
+    #[test]
+    fn test_struct_info_memory_kind_invalid_struct_info() {
+        let handle =
+            unsafe { mun_struct_info_memory_kind(StructInfoHandle::null(), ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'struct_info' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_struct_info_memory_kind_invalid_memory_kind() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo;"#,
+        );
+
+        let struct_info = get_struct_info_by_name(driver.runtime, "Foo");
+        let handle = unsafe { mun_struct_info_memory_kind(struct_info, ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'memory_kind' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_struct_info_memory_kind() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo {
+                bar: i8,
+                baz: f32,
+            }
+
+            pub struct(value) Bar;"#,
+        );
+
+        let foo_struct_info = get_struct_info_by_name(driver.runtime, "Foo");
+        let bar_struct_info = get_struct_info_by_name(driver.runtime, "Bar");
+
+        let mut foo_memory_kind = MaybeUninit::uninit();
+        let handle =
+            unsafe { mun_struct_info_memory_kind(foo_struct_info, foo_memory_kind.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let mut bar_memory_kind = MaybeUninit::uninit();
+        let handle =
+            unsafe { mun_struct_info_memory_kind(bar_struct_info, bar_memory_kind.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let foo_memory_kind = unsafe { foo_memory_kind.assume_init() };
+        let bar_memory_kind = unsafe { bar_memory_kind.assume_init() };
+
+        assert_eq!(foo_memory_kind, abi::StructMemoryKind::Gc);
+        assert_eq!(bar_memory_kind, abi::StructMemoryKind::Value);
+    }
+}
diff --git a/crates/mun_runtime_capi/src/type_info.rs b/crates/mun_runtime_capi/src/type_info.rs
new file mode 100644
index 000000000..e2b3536fa
--- /dev/null
+++ b/crates/mun_runtime_capi/src/type_info.rs
@@ -0,0 +1,646 @@
+//! Exposes type information using the C ABI.
+
+use crate::{error::ErrorHandle, struct_info::StructInfoHandle};
+use memory::{StructInfo, TypeInfo};
+use std::{
+    ffi::{c_void, CString},
+    os::raw::c_char,
+    ptr,
+    sync::Arc,
+};
+
+/// A C-style handle to a `TypeInfo`.
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct TypeInfoHandle(pub *const c_void);
+
+impl TypeInfoHandle {
+    /// A null handle.
+    pub fn null() -> Self {
+        Self(ptr::null())
+    }
+}
+
+/// A C-style handle to an array of `TypeInfoHandle`s.
+#[repr(C)]
+pub struct TypeInfoSpan {
+    /// Pointer to the start of the array buffer
+    pub data: *const TypeInfoHandle,
+    /// Length of the array (and capacity)
+    pub len: usize,
+}
+
+impl TypeInfoSpan {
+    /// Retrieves the `TypeInfoHandle` at the specified index, if within bounds.
+    /// Otherwise, returns null.
+    pub fn get(&self, index: usize) -> TypeInfoHandle {
+        if index < self.len {
+            // SAFETY: Bounds checking was performed
+            TypeInfoHandle(unsafe {
+                *(self.data as *const *const TypeInfo).add(index) as *const c_void
+            })
+        } else {
+            TypeInfoHandle::null()
+        }
+    }
+}
+
+/// Decrements the strong count of the `Arc<TypeInfo>` associated with `handle`.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `TypeInfoHandle` has already been
+/// deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_type_info_decrement_strong_count(handle: TypeInfoHandle) -> bool {
+    if !handle.0.is_null() {
+        Arc::decrement_strong_count(handle.0);
+        return true;
+    }
+
+    false
+}
+
+/// Increments the strong count of the `Arc<TypeInfo>` associated with `handle`.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `TypeInfoHandle` has already been
+/// deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_type_info_increment_strong_count(handle: TypeInfoHandle) -> bool {
+    if !handle.0.is_null() {
+        Arc::increment_strong_count(handle.0);
+        return true;
+    }
+
+    false
+}
+
+/// Retrieves the type's ID.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `TypeInfoHandle` has been
+/// deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_type_info_id(
+    type_info: TypeInfoHandle,
+    type_id: *mut abi::TypeId,
+) -> ErrorHandle {
+    let type_info = match (type_info.0 as *const TypeInfo).as_ref() {
+        Some(type_info) => type_info,
+        None => return ErrorHandle::new("Invalid argument: 'type_info' is null pointer."),
+    };
+
+    let type_id = match type_id.as_mut() {
+        Some(type_id) => type_id,
+        None => return ErrorHandle::new("Invalid argument: 'type_id' is null pointer."),
+    };
+
+    *type_id = type_info.id.clone();
+
+    ErrorHandle::default()
+}
+
+/// Retrieves the type's name.
+///
+/// # Safety
+///
+/// The caller is responsible for calling `mun_string_destroy` on the return pointer - if it is not null.
+///
+/// This function results in undefined behavior if the passed in `TypeInfoHandle` has been
+/// deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_type_info_name(type_info: TypeInfoHandle) -> *const c_char {
+    let type_info = match (type_info.0 as *const TypeInfo).as_ref() {
+        Some(type_info) => type_info,
+        None => return ptr::null(),
+    };
+
+    CString::new(type_info.name.clone()).unwrap().into_raw() as *const _
+}
+
+/// Retrieves the type's size.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `TypeInfoHandle` has been
+/// deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_type_info_size(
+    type_info: TypeInfoHandle,
+    size: *mut usize,
+) -> ErrorHandle {
+    let type_info = match (type_info.0 as *const TypeInfo).as_ref() {
+        Some(type_info) => type_info,
+        None => return ErrorHandle::new("Invalid argument: 'type_info' is null pointer."),
+    };
+
+    let size = match size.as_mut() {
+        Some(size) => size,
+        None => return ErrorHandle::new("Invalid argument: 'size' is null pointer."),
+    };
+
+    *size = type_info.layout.size();
+
+    ErrorHandle::default()
+}
+
+/// Retrieves the type's alignment.
+///
+/// # Safety
+///
+/// This function results in undefined behavior if the passed in `TypeInfoHandle` has been
+/// deallocated in a previous call to [`mun_type_info_decrement_strong_count`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_type_info_align(
+    type_info: TypeInfoHandle,
+    align: *mut usize,
+) -> ErrorHandle {
+    let type_info = match (type_info.0 as *const TypeInfo).as_ref() {
+        Some(type_info) => type_info,
+        None => return ErrorHandle::new("Invalid argument: 'type_info' is null pointer."),
+    };
+
+    let align = match align.as_mut() {
+        Some(align) => align,
+        None => return ErrorHandle::new("Invalid argument: 'align' is null pointer."),
+    };
+
+    *align = type_info.layout.align();
+
+    ErrorHandle::default()
+}
+
+/// An enum containing C-style handles a `TypeInfo`'s data.
+#[repr(u8)]
+#[derive(Clone, Copy, Debug)]
+pub enum TypeInfoData {
+    /// Primitive types (i.e. `()`, `bool`, `float`, `int`, etc.)
+    Primitive,
+    /// Struct types (i.e. record, tuple, or unit structs)
+    Struct(StructInfoHandle),
+}
+
+impl TypeInfoData {
+    /// Whether the type is a primitive.
+    #[cfg(test)]
+    fn is_primitive(&self) -> bool {
+        matches!(*self, TypeInfoData::Primitive)
+    }
+
+    /// Whether the type is a struct.
+    #[cfg(test)]
+    fn is_struct(&self) -> bool {
+        matches!(*self, TypeInfoData::Struct(_))
+    }
+
+    /// Returns the C-style handle to the struct information, if available.
+    #[cfg(test)]
+    pub(crate) fn as_struct(&self) -> Option<StructInfoHandle> {
+        if let TypeInfoData::Struct(handle) = self {
+            Some(*handle)
+        } else {
+            None
+        }
+    }
+}
+
+/// Retrieves the type's data.
+///
+/// # Safety
+///
+/// The original `TypeInfoHandle` needs to stay alive as long as the `TypeInfoData` lives. The
+/// `TypeInfoData` is destroyed at the same time as the `TypeInfo`. A `TypeInfo` might be destroyed
+/// through a call to [`mun_type_info_decrement_strong_count`].
+#[no_mangle]
+pub unsafe extern "C" fn mun_type_info_data(
+    type_info: TypeInfoHandle,
+    type_info_data: *mut TypeInfoData,
+) -> ErrorHandle {
+    let type_info = match (type_info.0 as *const TypeInfo).as_ref() {
+        Some(type_info) => type_info,
+        None => return ErrorHandle::new("Invalid argument: 'type_info' is null pointer."),
+    };
+
+    let type_info_data = match type_info_data.as_mut() {
+        Some(type_info_data) => type_info_data,
+        None => return ErrorHandle::new("Invalid argument: 'type_info_data' is null pointer."),
+    };
+
+    *type_info_data = match &type_info.data {
+        memory::TypeInfoData::Primitive => TypeInfoData::Primitive,
+        memory::TypeInfoData::Struct(s) => {
+            TypeInfoData::Struct(StructInfoHandle(s as *const StructInfo as *const c_void))
+        }
+    };
+
+    ErrorHandle::default()
+}
+
+/// Deallocates an span of `TypeInfo` that was allocated by the runtime.
+///
+/// # Safety
+///
+/// This function receives a span as parameter. Only when the spans data pointer is not null, its
+/// content will be deallocated. Passing pointers to invalid data or memory allocated by other
+/// processes, will lead to undefined behavior.
+#[no_mangle]
+pub unsafe extern "C" fn mun_type_info_span_destroy(array_handle: TypeInfoSpan) -> bool {
+    if array_handle.data.is_null() {
+        return false;
+    }
+
+    let data = array_handle.data as *mut *const TypeInfo;
+    let types = Vec::from_raw_parts(data, array_handle.len, array_handle.len);
+
+    types.into_iter().for_each(|ty| {
+        let _drop = Arc::from_raw(ty);
+    });
+
+    true
+}
+
+#[cfg(test)]
+pub(crate) mod tests {
+    use super::*;
+    use crate::function_info::mun_function_info_argument_types;
+    use crate::function_info::tests::get_fake_function_info;
+    use crate::{
+        error::mun_error_destroy,
+        runtime::{mun_runtime_get_type_info_by_name, RuntimeHandle},
+        test_util::TestDriver,
+    };
+    use memory::HasStaticTypeInfo;
+    use std::{
+        ffi::{CStr, CString},
+        mem::{self, MaybeUninit},
+        sync::Arc,
+    };
+
+    pub(crate) fn get_type_info_by_name<T: Into<Vec<u8>>>(
+        runtime: RuntimeHandle,
+        type_name: T,
+    ) -> TypeInfoHandle {
+        let type_name = CString::new(type_name).expect("Invalid type name");
+        let mut has_type_info = MaybeUninit::uninit();
+        let mut type_info = MaybeUninit::uninit();
+        let handle = unsafe {
+            mun_runtime_get_type_info_by_name(
+                runtime,
+                type_name.as_ptr(),
+                has_type_info.as_mut_ptr(),
+                type_info.as_mut_ptr(),
+            )
+        };
+        assert_eq!(handle.0, ptr::null());
+
+        let has_type_info = unsafe { has_type_info.assume_init() };
+        assert!(has_type_info);
+
+        unsafe { type_info.assume_init() }
+    }
+
+    #[test]
+    fn test_type_info_decrement_strong_count_invalid_type_info() {
+        assert_eq!(
+            unsafe { mun_type_info_decrement_strong_count(TypeInfoHandle::null()) },
+            false
+        );
+    }
+
+    #[test]
+    fn test_type_info_decrement_strong_count() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
+
+        let type_info_arc = unsafe { Arc::from_raw(type_info.0 as *const TypeInfo) };
+        let strong_count = Arc::strong_count(&type_info_arc);
+        assert!(strong_count > 0);
+
+        assert_eq!(
+            unsafe { mun_type_info_decrement_strong_count(type_info) },
+            true
+        );
+        assert_eq!(Arc::strong_count(&type_info_arc), strong_count - 1);
+
+        mem::forget(type_info_arc);
+    }
+
+    #[test]
+    fn test_type_info_increment_strong_count_invalid_type_info() {
+        assert_eq!(
+            unsafe { mun_type_info_increment_strong_count(TypeInfoHandle::null()) },
+            false
+        );
+    }
+
+    #[test]
+    fn test_type_info_increment_strong_count() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
+
+        let type_info_arc = unsafe { Arc::from_raw(type_info.0 as *const TypeInfo) };
+        let strong_count = Arc::strong_count(&type_info_arc);
+        assert!(strong_count > 0);
+
+        assert_eq!(
+            unsafe { mun_type_info_increment_strong_count(type_info) },
+            true
+        );
+        assert_eq!(Arc::strong_count(&type_info_arc), strong_count + 1);
+
+        mem::forget(type_info_arc);
+    }
+
+    #[test]
+    fn test_type_info_id_invalid_type_info() {
+        let handle = unsafe { mun_type_info_id(TypeInfoHandle::null(), ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'type_info' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_type_info_id_invalid_type_id() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
+        let handle = unsafe { mun_type_info_id(type_info, ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'type_id' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_type_info_id() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
+        let mut type_id = MaybeUninit::uninit();
+        let handle = unsafe { mun_type_info_id(type_info, type_id.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let type_id = unsafe { type_id.assume_init() };
+        assert_eq!(type_id, <i32>::type_info().id);
+    }
+
+    #[test]
+    fn test_type_info_name_invalid_type_info() {
+        let fn_ptr = unsafe { mun_type_info_name(TypeInfoHandle::null()) };
+        assert_eq!(fn_ptr, ptr::null());
+    }
+
+    #[test]
+    fn test_type_info_name() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
+        let name = unsafe { mun_type_info_name(type_info) };
+        assert_ne!(name, ptr::null());
+
+        let name = unsafe { CStr::from_ptr(name) }
+            .to_str()
+            .expect("Invalid type name.");
+
+        assert_eq!(name, "core::i32");
+    }
+
+    #[test]
+    fn test_type_info_size_invalid_type_info() {
+        let handle = unsafe { mun_type_info_size(TypeInfoHandle::null(), ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'type_info' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_type_info_size_invalid_size() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
+        let handle = unsafe { mun_type_info_size(type_info, ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'size' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_type_info_size() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
+        let mut size = MaybeUninit::uninit();
+        let handle = unsafe { mun_type_info_size(type_info, size.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let size = unsafe { size.assume_init() };
+        assert_eq!(size, mem::size_of::<i32>());
+    }
+
+    #[test]
+    fn test_type_info_align_invalid_type_info() {
+        let handle = unsafe { mun_type_info_align(TypeInfoHandle::null(), ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'type_info' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_type_info_align_invalid_align() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
+        let handle = unsafe { mun_type_info_align(type_info, ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'align' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_type_info_align() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
+        let mut align = MaybeUninit::uninit();
+        let handle = unsafe { mun_type_info_align(type_info, align.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let align = unsafe { align.assume_init() };
+        assert_eq!(align, mem::align_of::<i32>());
+    }
+
+    #[test]
+    fn test_type_info_data_invalid_type_info() {
+        let handle = unsafe { mun_type_info_data(TypeInfoHandle::null(), ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'type_info' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_type_info_data_invalid_type_info_data() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
+        let handle = unsafe { mun_type_info_data(type_info, ptr::null_mut()) };
+        assert_ne!(handle.0, ptr::null());
+
+        let message = unsafe { CStr::from_ptr(handle.0) };
+        assert_eq!(
+            message.to_str().unwrap(),
+            "Invalid argument: 'type_info_data' is null pointer."
+        );
+
+        unsafe { mun_error_destroy(handle) };
+    }
+
+    #[test]
+    fn test_type_info_data_primitive() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn main() -> i32 { 12345 }
+    "#,
+        );
+
+        let type_info = get_type_info_by_name(driver.runtime, "core::i32");
+        let mut type_info_data = MaybeUninit::uninit();
+        let handle = unsafe { mun_type_info_data(type_info, type_info_data.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let type_info_data = unsafe { type_info_data.assume_init() };
+        assert!(type_info_data.is_primitive());
+        assert!(!type_info_data.is_struct());
+    }
+
+    #[test]
+    fn test_type_info_data_struct() {
+        let driver = TestDriver::new(
+            r#"
+            pub struct Foo;
+
+            pub fn main() -> Foo { Foo }
+            "#,
+        );
+
+        let type_info = get_type_info_by_name(driver.runtime, "Foo");
+        let mut type_info_data = MaybeUninit::uninit();
+        let handle = unsafe { mun_type_info_data(type_info, type_info_data.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let type_info_data = unsafe { type_info_data.assume_init() };
+        assert!(!type_info_data.is_primitive());
+        assert!(type_info_data.is_struct());
+    }
+
+    #[test]
+    fn test_type_info_span_destroy() {
+        let driver = TestDriver::new(
+            r#"
+        pub fn add(a: i32, b: i32) -> i32 { a + b }
+        pub fn empty() -> i32 { 0 }
+    "#,
+        );
+
+        let fn_info = get_fake_function_info(driver.runtime, "add");
+        let mut arg_types = MaybeUninit::uninit();
+        let handle = unsafe { mun_function_info_argument_types(fn_info, arg_types.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let arg_types = unsafe { arg_types.assume_init() };
+        assert!(unsafe { mun_type_info_span_destroy(arg_types) });
+
+        let fn_info = get_fake_function_info(driver.runtime, "empty");
+        let mut arg_types = MaybeUninit::uninit();
+        let handle = unsafe { mun_function_info_argument_types(fn_info, arg_types.as_mut_ptr()) };
+        assert_eq!(handle.0, ptr::null());
+
+        let arg_types = unsafe { arg_types.assume_init() };
+        assert!(!unsafe { mun_type_info_span_destroy(arg_types) });
+    }
+}
diff --git a/crates/mun_vfs/src/monitor.rs b/crates/mun_vfs/src/monitor.rs
index 73eb09fc9..6377348b1 100644
--- a/crates/mun_vfs/src/monitor.rs
+++ b/crates/mun_vfs/src/monitor.rs
@@ -198,30 +198,12 @@ mod tests {
             ],
         };
 
-        assert_eq!(
-            config.contains_file(abs_manifest_dir.join("mod.mun")),
-            false
-        );
-        assert_eq!(
-            config.contains_file(abs_manifest_dir.join("src/mod.mun")),
-            true
-        );
-        assert_eq!(
-            config.contains_file(abs_manifest_dir.join("src/mod.rs")),
-            false
-        );
-        assert_eq!(
-            config.contains_file(abs_manifest_dir.join(".git/src/mod.mun")),
-            false
-        );
-        assert_eq!(
-            config.contains_file(abs_manifest_dir.join("src/.git/mod.mun")),
-            false
-        );
-        assert_eq!(
-            config.contains_file(abs_manifest_dir.join("src/.git/special_case/mod.mun")),
-            true
-        );
-        assert_eq!(config.contains_dir(abs_manifest_dir.join("src")), true);
+        assert!(!config.contains_file(abs_manifest_dir.join("mod.mun")));
+        assert!(config.contains_file(abs_manifest_dir.join("src/mod.mun")));
+        assert!(!config.contains_file(abs_manifest_dir.join("src/mod.rs")));
+        assert!(!config.contains_file(abs_manifest_dir.join(".git/src/mod.mun")));
+        assert!(!config.contains_file(abs_manifest_dir.join("src/.git/mod.mun")));
+        assert!(config.contains_file(abs_manifest_dir.join("src/.git/special_case/mod.mun")));
+        assert!(config.contains_dir(abs_manifest_dir.join("src")));
     }
 }


Test patch:
diff --git a/cpp/tests/runtime.cc b/cpp/tests/runtime.cc
index 9603381cd..230c74de4 100644
--- a/cpp/tests/runtime.cc
+++ b/cpp/tests/runtime.cc
@@ -12,7 +12,8 @@ inline std::string get_munlib_path(std::string_view name) {
 
 TEST_CASE("runtime can be constructed", "[runtime]") {
     mun::Error err;
-    if (auto runtime = mun::make_runtime(get_munlib_path("fibonacci/target/mod.munlib"), {}, &err)) {
+    if (auto runtime =
+            mun::make_runtime(get_munlib_path("fibonacci/target/mod.munlib"), {}, &err)) {
         REQUIRE(!err);
     } else {
         REQUIRE(err);
@@ -22,11 +23,12 @@ TEST_CASE("runtime can be constructed", "[runtime]") {
 
 TEST_CASE("runtime can find `FunctionInfo`", "[runtime]") {
     mun::Error err;
-    if (auto runtime = mun::make_runtime(get_munlib_path("fibonacci/target/mod.munlib"), {}, &err)) {
+    if (auto runtime =
+            mun::make_runtime(get_munlib_path("fibonacci/target/mod.munlib"), {}, &err)) {
         REQUIRE(!err);
         REQUIRE(runtime.has_value());
 
-        if (auto function_info = runtime->find_function_definition("fibonacci", &err)) {
+        if (auto function_info = runtime->find_function_info("fibonacci", &err)) {
             REQUIRE(!err);
         } else {
             REQUIRE(err);
@@ -41,7 +43,8 @@ TEST_CASE("runtime can find `FunctionInfo`", "[runtime]") {
 // TODO: Test hot reloading
 TEST_CASE("runtime can update", "[runtime]") {
     mun::Error err;
-    if (auto runtime = mun::make_runtime(get_munlib_path("fibonacci/target/mod.munlib"), {}, &err)) {
+    if (auto runtime =
+            mun::make_runtime(get_munlib_path("fibonacci/target/mod.munlib"), {}, &err)) {
         REQUIRE(!err);
 
         runtime->update(&err);
diff --git a/crates/mun_abi/src/test_utils.rs b/crates/mun_abi/src/test_utils.rs
index 4fd7cb4ef..f08b2eed4 100644
--- a/crates/mun_abi/src/test_utils.rs
+++ b/crates/mun_abi/src/test_utils.rs
@@ -1,15 +1,18 @@
 use crate::{
     AssemblyInfo, DispatchTable, FunctionDefinition, FunctionPrototype, FunctionSignature, Guid,
-    ModuleInfo, StructInfo, StructMemoryKind, TypeInfo, TypeInfoData,
+    HasStaticTypeInfo, ModuleInfo, StructInfo, StructMemoryKind, TypeId, TypeInfo, TypeInfoData,
+    TypeLut,
 };
 use std::{
-    ffi::{c_void, CStr},
+    ffi::{self, CStr},
     os::raw::c_char,
-    ptr,
 };
 
 pub(crate) const FAKE_TYPE_GUID: Guid =
     Guid([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
+pub(crate) const FAKE_TYPE_ID: TypeId = TypeId {
+    guid: FAKE_TYPE_GUID,
+};
 pub(crate) const FAKE_DEPENDENCY: &str = "path/to/dependency.munlib";
 pub(crate) const FAKE_FIELD_NAME: &str = "field_name";
 pub(crate) const FAKE_FN_NAME: &str = "fn_name";
@@ -20,19 +23,36 @@ pub(crate) const FAKE_TYPE_NAME: &str = "TypeName";
 pub(crate) fn fake_assembly_info(
     symbols: ModuleInfo,
     dispatch_table: DispatchTable,
+    type_lut: TypeLut,
     dependencies: &[*const c_char],
 ) -> AssemblyInfo {
     AssemblyInfo {
         symbols,
         dispatch_table,
+        type_lut,
         dependencies: dependencies.as_ptr(),
         num_dependencies: dependencies.len() as u32,
     }
 }
 
+pub(crate) fn fake_type_lut(
+    type_ids: &[TypeId],
+    type_handles: &mut [*const ffi::c_void],
+    type_names: &[*const c_char],
+) -> TypeLut {
+    assert_eq!(type_ids.len(), type_handles.len());
+
+    TypeLut {
+        type_ids: type_ids.as_ptr(),
+        type_handles: type_handles.as_mut_ptr(),
+        type_names: type_names.as_ptr(),
+        num_entries: type_ids.len() as u32,
+    }
+}
+
 pub(crate) fn fake_dispatch_table(
     fn_prototypes: &[FunctionPrototype],
-    fn_ptrs: &mut [*const c_void],
+    fn_ptrs: &mut [*const ffi::c_void],
 ) -> DispatchTable {
     assert_eq!(fn_prototypes.len(), fn_ptrs.len());
 
@@ -44,20 +64,20 @@ pub(crate) fn fake_dispatch_table(
 }
 
 pub(crate) fn fake_fn_signature(
-    arg_types: &[&TypeInfo],
-    return_type: Option<&TypeInfo>,
+    arg_types: &[TypeId],
+    return_type: Option<TypeId>,
 ) -> FunctionSignature {
     FunctionSignature {
-        arg_types: arg_types.as_ptr().cast::<*const TypeInfo>(),
-        return_type: return_type.map_or(ptr::null(), |t| t as *const TypeInfo),
+        arg_types: arg_types.as_ptr(),
+        return_type: return_type.unwrap_or_else(|| <()>::type_info().id.clone()),
         num_arg_types: arg_types.len() as u16,
     }
 }
 
 pub(crate) fn fake_fn_prototype(
     name: &CStr,
-    arg_types: &[&TypeInfo],
-    return_type: Option<&TypeInfo>,
+    arg_types: &[TypeId],
+    return_type: Option<TypeId>,
 ) -> FunctionPrototype {
     FunctionPrototype {
         name: name.as_ptr(),
@@ -68,20 +88,20 @@ pub(crate) fn fake_fn_prototype(
 pub(crate) fn fake_module_info(
     path: &CStr,
     functions: &[FunctionDefinition],
-    types: &[&TypeInfo],
+    types: &[TypeInfo],
 ) -> ModuleInfo {
     ModuleInfo {
         path: path.as_ptr(),
         functions: functions.as_ptr(),
         num_functions: functions.len() as u32,
-        types: types.as_ptr().cast::<*const TypeInfo>(),
+        types: types.as_ptr(),
         num_types: types.len() as u32,
     }
 }
 
 pub(crate) fn fake_struct_info(
     field_names: &[*const c_char],
-    field_types: &[&TypeInfo],
+    field_types: &[TypeId],
     field_offsets: &[u16],
     memory_kind: StructMemoryKind,
 ) -> StructInfo {
@@ -90,7 +110,7 @@ pub(crate) fn fake_struct_info(
 
     StructInfo {
         field_names: field_names.as_ptr(),
-        field_types: field_types.as_ptr().cast::<*const TypeInfo>(),
+        field_types: field_types.as_ptr(),
         field_offsets: field_offsets.as_ptr(),
         num_fields: field_names.len() as u16,
         memory_kind,
@@ -104,7 +124,9 @@ pub(crate) fn fake_type_info(
     data: TypeInfoData,
 ) -> TypeInfo {
     TypeInfo {
-        guid: FAKE_TYPE_GUID,
+        id: TypeId {
+            guid: Guid::from(name.to_bytes()),
+        },
         name: name.as_ptr(),
         size_in_bits: size,
         alignment,
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_f32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_f32.snap
index afed58fc1..a08c65cd1 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_f32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_f32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn add(a: f32, b: f32) -> f32 { a + b }\n    pub fn subtract(a: f32, b: f32) -> f32 { a - b }\n    pub fn multiply(a: f32, b: f32) -> f32 { a * b }\n    pub fn divide(a: f32, b: f32) -> f32 { a / b }\n    pub fn remainder(a: f32, b: f32) -> f32 { a % b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [15 x i64*]
 
 define float @add(float %0, float %1) {
 body:
@@ -47,10 +43,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::f32>::name" = private unnamed_addr constant [10 x i8] c"core::f32\00"
-@"type_info::<core::f32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"P\19b7\A8k\F2\81P\FB\83\F5P\B0\82!", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::f32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f32>"]
+@global_type_lookup_table = global [15 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_f64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_f64.snap
index b85529b09..c3def375f 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_f64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_f64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn add(a: f64, b: f64) -> f64 { a + b }\n    pub fn subtract(a: f64, b: f64) -> f64 { a - b }\n    pub fn multiply(a: f64, b: f64) -> f64 { a * b }\n    pub fn divide(a: f64, b: f64) -> f64 { a / b }\n    pub fn remainder(a: f64, b: f64) -> f64 { a % b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [15 x i64*]
 
 define double @add(double %0, double %1) {
 body:
@@ -47,10 +43,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::f64>::name" = private unnamed_addr constant [10 x i8] c"core::f64\00"
-@"type_info::<core::f64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"`\DBF\9C?YJ%G\AD4\9F\D5\92%A", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::f64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f64>"]
+@global_type_lookup_table = global [15 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i128.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i128.snap
index 6f79a393a..c8a4b524d 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i128.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i128.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn add(a: i128, b: i128) -> i128 { a + b }\n    pub fn subtract(a: i128, b: i128) -> i128 { a - b }\n    pub fn multiply(a: i128, b: i128) -> i128 { a * b }\n    pub fn divide(a: i128, b: i128) -> i128 { a / b }\n    pub fn remainder(a: i128, b: i128) -> i128 { a % b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [15 x i64*]
 
 define i128 @add(i128 %0, i128 %1) {
 body:
@@ -47,10 +43,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i128>::name" = private unnamed_addr constant [11 x i8] c"core::i128\00"
-@"type_info::<core::i128>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\BDkp\09RRM\EBc\02\A0\DB47\A7\E3", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::i128>::name", i32 0, i32 0), [48 x i8] c"\80\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i128>"]
+@global_type_lookup_table = global [15 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i16.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i16.snap
index 0afe320f4..68901b44c 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i16.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i16.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn add(a: i16, b: i16) -> i16 { a + b }\n    pub fn subtract(a: i16, b: i16) -> i16 { a - b }\n    pub fn multiply(a: i16, b: i16) -> i16 { a * b }\n    pub fn divide(a: i16, b: i16) -> i16 { a / b }\n    pub fn remainder(a: i16, b: i16) -> i16 { a % b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [15 x i64*]
 
 define i16 @add(i16 %0, i16 %1) {
 body:
@@ -47,10 +43,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i16>::name" = private unnamed_addr constant [10 x i8] c"core::i16\00"
-@"type_info::<core::i16>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\05\CD|\F8Bv\D8\B1\E8\8B\8C\D8\8D\B5\89\B0", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i16>::name", i32 0, i32 0), [48 x i8] c"\10\00\00\00\02\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i16>"]
+@global_type_lookup_table = global [15 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i32.snap
index 7148722d0..e747bcc00 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn add(a: i32, b: i32) -> i32 { a + b }\n    pub fn subtract(a: i32, b: i32) -> i32 { a - b }\n    pub fn multiply(a: i32, b: i32) -> i32 { a * b }\n    pub fn divide(a: i32, b: i32) -> i32 { a / b }\n    pub fn remainder(a: i32, b: i32) -> i32 { a % b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [15 x i64*]
 
 define i32 @add(i32 %0, i32 %1) {
 body:
@@ -47,10 +43,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [15 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i64.snap
index 2c9dd6cf0..61cf6bdd7 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn add(a: i64, b: i64) -> i64 { a + b }\n    pub fn subtract(a: i64, b: i64) -> i64 { a - b }\n    pub fn multiply(a: i64, b: i64) -> i64 { a * b }\n    pub fn divide(a: i64, b: i64) -> i64 { a / b }\n    pub fn remainder(a: i64, b: i64) -> i64 { a % b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [15 x i64*]
 
 define i64 @add(i64 %0, i64 %1) {
 body:
@@ -47,10 +43,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i64>::name" = private unnamed_addr constant [10 x i8] c"core::i64\00"
-@"type_info::<core::i64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"G\13;t\97j8\18\D7M\83`\1D\C8\19%", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i64>"]
+@global_type_lookup_table = global [15 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i8.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i8.snap
index e6aa14498..b6d7ff793 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i8.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_i8.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn add(a: i8, b: i8) -> i8 { a + b }\n    pub fn subtract(a: i8, b: i8) -> i8 { a - b }\n    pub fn multiply(a: i8, b: i8) -> i8 { a * b }\n    pub fn divide(a: i8, b: i8) -> i8 { a / b }\n    pub fn remainder(a: i8, b: i8) -> i8 { a % b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [15 x i64*]
 
 define i8 @add(i8 %0, i8 %1) {
 body:
@@ -47,10 +43,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i8>::name" = private unnamed_addr constant [9 x i8] c"core::i8\00"
-@"type_info::<core::i8>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\EF\C4\B1Z\E7\12\B1\91q\F1\0B\80U\FC\A6\0F", i8* getelementptr inbounds ([9 x i8], [9 x i8]* @"type_info::<core::i8>::name", i32 0, i32 0), [48 x i8] c"\08\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i8>"]
+@global_type_lookup_table = global [15 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u128.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u128.snap
index edf0b7e36..6faff10f8 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u128.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u128.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn add(a: u128, b: u128) -> u128 { a + b }\n    pub fn subtract(a: u128, b: u128) -> u128 { a - b }\n    pub fn multiply(a: u128, b: u128) -> u128 { a * b }\n    pub fn divide(a: u128, b: u128) -> u128 { a / b }\n    pub fn remainder(a: u128, b: u128) -> u128 { a % b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [15 x i64*]
 
 define i128 @add(i128 %0, i128 %1) {
 body:
@@ -47,10 +43,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u128>::name" = private unnamed_addr constant [11 x i8] c"core::u128\00"
-@"type_info::<core::u128>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\E67\1BU\E9k\95\93d\14}\1C\96S\95\F0", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::u128>::name", i32 0, i32 0), [48 x i8] c"\80\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u128>"]
+@global_type_lookup_table = global [15 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u16.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u16.snap
index f891f95ba..2b7f3f293 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u16.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u16.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn add(a: u16, b: u16) -> u16 { a + b }\n    pub fn subtract(a: u16, b: u16) -> u16 { a - b }\n    pub fn multiply(a: u16, b: u16) -> u16 { a * b }\n    pub fn divide(a: u16, b: u16) -> u16 { a / b }\n    pub fn remainder(a: u16, b: u16) -> u16 { a % b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [15 x i64*]
 
 define i16 @add(i16 %0, i16 %1) {
 body:
@@ -47,10 +43,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u16>::name" = private unnamed_addr constant [10 x i8] c"core::u16\00"
-@"type_info::<core::u16>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"0\01\BC\BBK\E0\F2\7F&l\01\CD|q\F2\B3", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u16>::name", i32 0, i32 0), [48 x i8] c"\10\00\00\00\02\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u16>"]
+@global_type_lookup_table = global [15 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u32.snap
index fe475347a..16680500b 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn add(a: u32, b: u32) -> u32 { a + b }\n    pub fn subtract(a: u32, b: u32) -> u32 { a - b }\n    pub fn multiply(a: u32, b: u32) -> u32 { a * b }\n    pub fn divide(a: u32, b: u32) -> u32 { a / b }\n    pub fn remainder(a: u32, b: u32) -> u32 { a % b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [15 x i64*]
 
 define i32 @add(i32 %0, i32 %1) {
 body:
@@ -47,10 +43,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u32>::name" = private unnamed_addr constant [10 x i8] c"core::u32\00"
-@"type_info::<core::u32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"daz5d\A6\BE\88\81=&Y\A1+\C6\1D", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u32>"]
+@global_type_lookup_table = global [15 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u64.snap
index 229cce876..067883a43 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn add(a: u64, b: u64) -> u64 { a + b }\n    pub fn subtract(a: u64, b: u64) -> u64 { a - b }\n    pub fn multiply(a: u64, b: u64) -> u64 { a * b }\n    pub fn divide(a: u64, b: u64) -> u64 { a / b }\n    pub fn remainder(a: u64, b: u64) -> u64 { a % b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [15 x i64*]
 
 define i64 @add(i64 %0, i64 %1) {
 body:
@@ -47,10 +43,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u64>::name" = private unnamed_addr constant [10 x i8] c"core::u64\00"
-@"type_info::<core::u64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\A6\E7g \D1\8B\1Aq`\1F\1E\07\BB5@q", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u64>"]
+@global_type_lookup_table = global [15 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u8.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u8.snap
index ca4b771ba..8ba36d7bd 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u8.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__arithmetic_op_u8.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn add(a: u8, b: u8) -> u8 { a + b }\n    pub fn subtract(a: u8, b: u8) -> u8 { a - b }\n    pub fn multiply(a: u8, b: u8) -> u8 { a * b }\n    pub fn divide(a: u8, b: u8) -> u8 { a / b }\n    pub fn remainder(a: u8, b: u8) -> u8 { a % b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [15 x i64*]
 
 define i8 @add(i8 %0, i8 %1) {
 body:
@@ -47,10 +43,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u8>::name" = private unnamed_addr constant [9 x i8] c"core::u8\00"
-@"type_info::<core::u8>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\A0y\A7S\B6(n\F7f&H\E1\F9\AD\04>", i8* getelementptr inbounds ([9 x i8], [9 x i8]* @"type_info::<core::u8>::name", i32 0, i32 0), [48 x i8] c"\08\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u8>"]
+@global_type_lookup_table = global [15 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_bool.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_bool.snap
index b6e03e451..2b5e267c2 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_bool.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_bool.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_bitand(a: bool, b: bool) -> bool {\n        a &= b;\n        a\n    }\n    pub fn assign_bitor(a: bool, b: bool) -> bool {\n        a |= b;\n        a\n    }\n    pub fn assign_bitxor(a: bool, b: bool) -> bool {\n        a ^= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [9 x i64*]
 
 define i1 @assign_bitand(i1 %0, i1 %1) {
 body:
@@ -35,10 +31,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>"]
+@global_type_lookup_table = global [9 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i128.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i128.snap
index 93cb68d56..d776b2698 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i128.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i128.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_bitand(a: i128, b: i128) -> i128 {\n        a &= b;\n        a\n    }\n    pub fn assign_bitor(a: i128, b: i128) -> i128 {\n        a |= b;\n        a\n    }\n    pub fn assign_bitxor(a: i128, b: i128) -> i128 {\n        a ^= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [9 x i64*]
 
 define i128 @assign_bitand(i128 %0, i128 %1) {
 body:
@@ -35,10 +31,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i128>::name" = private unnamed_addr constant [11 x i8] c"core::i128\00"
-@"type_info::<core::i128>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\BDkp\09RRM\EBc\02\A0\DB47\A7\E3", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::i128>::name", i32 0, i32 0), [48 x i8] c"\80\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i128>"]
+@global_type_lookup_table = global [9 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i16.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i16.snap
index 269fa27d3..5dc78fa1f 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i16.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i16.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_bitand(a: i16, b: i16) -> i16 {\n        a &= b;\n        a\n    }\n    pub fn assign_bitor(a: i16, b: i16) -> i16 {\n        a |= b;\n        a\n    }\n    pub fn assign_bitxor(a: i16, b: i16) -> i16 {\n        a ^= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [9 x i64*]
 
 define i16 @assign_bitand(i16 %0, i16 %1) {
 body:
@@ -35,10 +31,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i16>::name" = private unnamed_addr constant [10 x i8] c"core::i16\00"
-@"type_info::<core::i16>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\05\CD|\F8Bv\D8\B1\E8\8B\8C\D8\8D\B5\89\B0", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i16>::name", i32 0, i32 0), [48 x i8] c"\10\00\00\00\02\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i16>"]
+@global_type_lookup_table = global [9 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i32.snap
index b40d8824c..e7d8e53d7 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_bitand(a: i32, b: i32) -> i32 {\n        a &= b;\n        a\n    }\n    pub fn assign_bitor(a: i32, b: i32) -> i32 {\n        a |= b;\n        a\n    }\n    pub fn assign_bitxor(a: i32, b: i32) -> i32 {\n        a ^= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [9 x i64*]
 
 define i32 @assign_bitand(i32 %0, i32 %1) {
 body:
@@ -35,10 +31,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [9 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i64.snap
index d8287cdfe..fee8ee19c 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_bitand(a: i64, b: i64) -> i64 {\n        a &= b;\n        a\n    }\n    pub fn assign_bitor(a: i64, b: i64) -> i64 {\n        a |= b;\n        a\n    }\n    pub fn assign_bitxor(a: i64, b: i64) -> i64 {\n        a ^= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [9 x i64*]
 
 define i64 @assign_bitand(i64 %0, i64 %1) {
 body:
@@ -35,10 +31,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i64>::name" = private unnamed_addr constant [10 x i8] c"core::i64\00"
-@"type_info::<core::i64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"G\13;t\97j8\18\D7M\83`\1D\C8\19%", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i64>"]
+@global_type_lookup_table = global [9 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i8.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i8.snap
index 2348680c4..c187fbfb3 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i8.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_i8.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_bitand(a: i8, b: i8) -> i8 {\n        a &= b;\n        a\n    }\n    pub fn assign_bitor(a: i8, b: i8) -> i8 {\n        a |= b;\n        a\n    }\n    pub fn assign_bitxor(a: i8, b: i8) -> i8 {\n        a ^= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [9 x i64*]
 
 define i8 @assign_bitand(i8 %0, i8 %1) {
 body:
@@ -35,10 +31,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i8>::name" = private unnamed_addr constant [9 x i8] c"core::i8\00"
-@"type_info::<core::i8>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\EF\C4\B1Z\E7\12\B1\91q\F1\0B\80U\FC\A6\0F", i8* getelementptr inbounds ([9 x i8], [9 x i8]* @"type_info::<core::i8>::name", i32 0, i32 0), [48 x i8] c"\08\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i8>"]
+@global_type_lookup_table = global [9 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u128.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u128.snap
index 234766a83..b3f6031cb 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u128.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u128.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_bitand(a: u128, b: u128) -> u128 {\n        a &= b;\n        a\n    }\n    pub fn assign_bitor(a: u128, b: u128) -> u128 {\n        a |= b;\n        a\n    }\n    pub fn assign_bitxor(a: u128, b: u128) -> u128 {\n        a ^= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [9 x i64*]
 
 define i128 @assign_bitand(i128 %0, i128 %1) {
 body:
@@ -35,10 +31,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u128>::name" = private unnamed_addr constant [11 x i8] c"core::u128\00"
-@"type_info::<core::u128>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\E67\1BU\E9k\95\93d\14}\1C\96S\95\F0", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::u128>::name", i32 0, i32 0), [48 x i8] c"\80\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u128>"]
+@global_type_lookup_table = global [9 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u16.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u16.snap
index 2a346c86d..3ca2a8da9 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u16.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u16.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_bitand(a: u16, b: u16) -> u16 {\n        a &= b;\n        a\n    }\n    pub fn assign_bitor(a: u16, b: u16) -> u16 {\n        a |= b;\n        a\n    }\n    pub fn assign_bitxor(a: u16, b: u16) -> u16 {\n        a ^= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [9 x i64*]
 
 define i16 @assign_bitand(i16 %0, i16 %1) {
 body:
@@ -35,10 +31,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u16>::name" = private unnamed_addr constant [10 x i8] c"core::u16\00"
-@"type_info::<core::u16>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"0\01\BC\BBK\E0\F2\7F&l\01\CD|q\F2\B3", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u16>::name", i32 0, i32 0), [48 x i8] c"\10\00\00\00\02\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u16>"]
+@global_type_lookup_table = global [9 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u32.snap
index 6ffb7afaf..282d51326 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_bitand(a: u32, b: u32) -> u32 {\n        a &= b;\n        a\n    }\n    pub fn assign_bitor(a: u32, b: u32) -> u32 {\n        a |= b;\n        a\n    }\n    pub fn assign_bitxor(a: u32, b: u32) -> u32 {\n        a ^= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [9 x i64*]
 
 define i32 @assign_bitand(i32 %0, i32 %1) {
 body:
@@ -35,10 +31,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u32>::name" = private unnamed_addr constant [10 x i8] c"core::u32\00"
-@"type_info::<core::u32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"daz5d\A6\BE\88\81=&Y\A1+\C6\1D", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u32>"]
+@global_type_lookup_table = global [9 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u64.snap
index c4ff0f514..f89780dea 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_bitand(a: u64, b: u64) -> u64 {\n        a &= b;\n        a\n    }\n    pub fn assign_bitor(a: u64, b: u64) -> u64 {\n        a |= b;\n        a\n    }\n    pub fn assign_bitxor(a: u64, b: u64) -> u64 {\n        a ^= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [9 x i64*]
 
 define i64 @assign_bitand(i64 %0, i64 %1) {
 body:
@@ -35,10 +31,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u64>::name" = private unnamed_addr constant [10 x i8] c"core::u64\00"
-@"type_info::<core::u64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\A6\E7g \D1\8B\1Aq`\1F\1E\07\BB5@q", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u64>"]
+@global_type_lookup_table = global [9 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u8.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u8.snap
index 3ca04bb21..68c73b303 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u8.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_bit_op_u8.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_bitand(a: u8, b: u8) -> u8 {\n        a &= b;\n        a\n    }\n    pub fn assign_bitor(a: u8, b: u8) -> u8 {\n        a |= b;\n        a\n    }\n    pub fn assign_bitxor(a: u8, b: u8) -> u8 {\n        a ^= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [9 x i64*]
 
 define i8 @assign_bitand(i8 %0, i8 %1) {
 body:
@@ -35,10 +31,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u8>::name" = private unnamed_addr constant [9 x i8] c"core::u8\00"
-@"type_info::<core::u8>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\A0y\A7S\B6(n\F7f&H\E1\F9\AD\04>", i8* getelementptr inbounds ([9 x i8], [9 x i8]* @"type_info::<core::u8>::name", i32 0, i32 0), [48 x i8] c"\08\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u8>"]
+@global_type_lookup_table = global [9 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i128.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i128.snap
index 3f77800bf..b3a9fa1ae 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i128.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i128.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_leftshift(a: i128, b: i128) -> i128 {\n        a <<= b;\n        a\n    }\n    pub fn assign_rightshift(a: i128, b: i128) -> i128 {\n        a >>= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i128 @assign_leftshift(i128 %0, i128 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i128>::name" = private unnamed_addr constant [11 x i8] c"core::i128\00"
-@"type_info::<core::i128>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\BDkp\09RRM\EBc\02\A0\DB47\A7\E3", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::i128>::name", i32 0, i32 0), [48 x i8] c"\80\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i128>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i16.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i16.snap
index 9c7bc08a7..a52585eaf 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i16.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i16.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_leftshift(a: i16, b: i16) -> i16 {\n        a <<= b;\n        a\n    }\n    pub fn assign_rightshift(a: i16, b: i16) -> i16 {\n        a >>= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i16 @assign_leftshift(i16 %0, i16 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i16>::name" = private unnamed_addr constant [10 x i8] c"core::i16\00"
-@"type_info::<core::i16>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\05\CD|\F8Bv\D8\B1\E8\8B\8C\D8\8D\B5\89\B0", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i16>::name", i32 0, i32 0), [48 x i8] c"\10\00\00\00\02\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i16>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i32.snap
index b786e8c37..e2ab1a7ab 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_leftshift(a: i32, b: i32) -> i32 {\n        a <<= b;\n        a\n    }\n    pub fn assign_rightshift(a: i32, b: i32) -> i32 {\n        a >>= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i32 @assign_leftshift(i32 %0, i32 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i64.snap
index 2290cd2a8..81d102112 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_leftshift(a: i64, b: i64) -> i64 {\n        a <<= b;\n        a\n    }\n    pub fn assign_rightshift(a: i64, b: i64) -> i64 {\n        a >>= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i64 @assign_leftshift(i64 %0, i64 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i64>::name" = private unnamed_addr constant [10 x i8] c"core::i64\00"
-@"type_info::<core::i64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"G\13;t\97j8\18\D7M\83`\1D\C8\19%", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i64>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i8.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i8.snap
index ede1ce84e..686da4872 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i8.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_i8.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_leftshift(a: i8, b: i8) -> i8 {\n        a <<= b;\n        a\n    }\n    pub fn assign_rightshift(a: i8, b: i8) -> i8 {\n        a >>= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i8 @assign_leftshift(i8 %0, i8 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i8>::name" = private unnamed_addr constant [9 x i8] c"core::i8\00"
-@"type_info::<core::i8>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\EF\C4\B1Z\E7\12\B1\91q\F1\0B\80U\FC\A6\0F", i8* getelementptr inbounds ([9 x i8], [9 x i8]* @"type_info::<core::i8>::name", i32 0, i32 0), [48 x i8] c"\08\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i8>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u128.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u128.snap
index 235b431c9..ed22aff7e 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u128.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u128.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_leftshift(a: u128, b: u128) -> u128 {\n        a <<= b;\n        a\n    }\n    pub fn assign_rightshift(a: u128, b: u128) -> u128 {\n        a >>= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i128 @assign_leftshift(i128 %0, i128 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u128>::name" = private unnamed_addr constant [11 x i8] c"core::u128\00"
-@"type_info::<core::u128>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\E67\1BU\E9k\95\93d\14}\1C\96S\95\F0", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::u128>::name", i32 0, i32 0), [48 x i8] c"\80\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u128>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u16.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u16.snap
index 3129033dd..74768fbda 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u16.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u16.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_leftshift(a: u16, b: u16) -> u16 {\n        a <<= b;\n        a\n    }\n    pub fn assign_rightshift(a: u16, b: u16) -> u16 {\n        a >>= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i16 @assign_leftshift(i16 %0, i16 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u16>::name" = private unnamed_addr constant [10 x i8] c"core::u16\00"
-@"type_info::<core::u16>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"0\01\BC\BBK\E0\F2\7F&l\01\CD|q\F2\B3", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u16>::name", i32 0, i32 0), [48 x i8] c"\10\00\00\00\02\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u16>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u32.snap
index b74e6ac67..181c93fb8 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_leftshift(a: u32, b: u32) -> u32 {\n        a <<= b;\n        a\n    }\n    pub fn assign_rightshift(a: u32, b: u32) -> u32 {\n        a >>= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i32 @assign_leftshift(i32 %0, i32 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u32>::name" = private unnamed_addr constant [10 x i8] c"core::u32\00"
-@"type_info::<core::u32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"daz5d\A6\BE\88\81=&Y\A1+\C6\1D", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u32>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u64.snap
index 78bd7a665..fbf9d3323 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_leftshift(a: u64, b: u64) -> u64 {\n        a <<= b;\n        a\n    }\n    pub fn assign_rightshift(a: u64, b: u64) -> u64 {\n        a >>= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i64 @assign_leftshift(i64 %0, i64 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u64>::name" = private unnamed_addr constant [10 x i8] c"core::u64\00"
-@"type_info::<core::u64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\A6\E7g \D1\8B\1Aq`\1F\1E\07\BB5@q", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u64>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u8.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u8.snap
index b4ed5bd82..3d5c2fc9c 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u8.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assign_shift_op_u8.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign_leftshift(a: u8, b: u8) -> u8 {\n        a <<= b;\n        a\n    }\n    pub fn assign_rightshift(a: u8, b: u8) -> u8 {\n        a >>= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i8 @assign_leftshift(i8 %0, i8 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u8>::name" = private unnamed_addr constant [9 x i8] c"core::u8\00"
-@"type_info::<core::u8>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\A0y\A7S\B6(n\F7f&H\E1\F9\AD\04>", i8* getelementptr inbounds ([9 x i8], [9 x i8]* @"type_info::<core::u8>::name", i32 0, i32 0), [48 x i8] c"\08\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u8>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_bool.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_bool.snap
index a536484ef..7ea6f9512 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_bool.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_bool.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign(a: bool, b: bool) -> bool {\n        a = b;\n        a\n    }\n    // TODO: Add errors\n    // a += b;\n    // a *= b;\n    // a -= b;\n    // a /= b;\n    // a %= b;\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [3 x i64*]
 
 define i1 @assign(i1 %0, i1 %1) {
 body:
@@ -22,10 +18,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>"]
+@global_type_lookup_table = global [3 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_f32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_f32.snap
index 0ecc9b4a2..296db3a05 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_f32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_f32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign(a: f32, b: f32) -> f32 {\n        a = b;\n        a\n    }\n    pub fn assign_add(a: f32, b: f32) -> f32 {\n        a += b;\n        a\n    }\n    pub fn assign_subtract(a: f32, b: f32) -> f32 {\n        a -= b;\n        a\n    }\n    pub fn assign_multiply(a: f32, b: f32) -> f32 {\n        a *= b;\n        a\n    }\n    pub fn assign_divide(a: f32, b: f32) -> f32 {\n        a /= b;\n        a\n    }\n    pub fn assign_remainder(a: f32, b: f32) -> f32 {\n        a %= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define float @assign(float %0, float %1) {
 body:
@@ -52,10 +48,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::f32>::name" = private unnamed_addr constant [10 x i8] c"core::f32\00"
-@"type_info::<core::f32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"P\19b7\A8k\F2\81P\FB\83\F5P\B0\82!", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::f32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f32>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_f64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_f64.snap
index 4e048ad76..aa72d2e79 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_f64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_f64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign(a: f64, b: f64) -> f64 {\n        a = b;\n        a\n    }\n    pub fn assign_add(a: f64, b: f64) -> f64 {\n        a += b;\n        a\n    }\n    pub fn assign_subtract(a: f64, b: f64) -> f64 {\n        a -= b;\n        a\n    }\n    pub fn assign_multiply(a: f64, b: f64) -> f64 {\n        a *= b;\n        a\n    }\n    pub fn assign_divide(a: f64, b: f64) -> f64 {\n        a /= b;\n        a\n    }\n    pub fn assign_remainder(a: f64, b: f64) -> f64 {\n        a %= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define double @assign(double %0, double %1) {
 body:
@@ -52,10 +48,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::f64>::name" = private unnamed_addr constant [10 x i8] c"core::f64\00"
-@"type_info::<core::f64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"`\DBF\9C?YJ%G\AD4\9F\D5\92%A", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::f64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f64>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i128.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i128.snap
index 8fc4cbf6a..e49b71e7c 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i128.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i128.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign(a: i128, b: i128) -> i128 {\n        a = b;\n        a\n    }\n    pub fn assign_add(a: i128, b: i128) -> i128 {\n        a += b;\n        a\n    }\n    pub fn assign_subtract(a: i128, b: i128) -> i128 {\n        a -= b;\n        a\n    }\n    pub fn assign_multiply(a: i128, b: i128) -> i128 {\n        a *= b;\n        a\n    }\n    pub fn assign_divide(a: i128, b: i128) -> i128 {\n        a /= b;\n        a\n    }\n    pub fn assign_remainder(a: i128, b: i128) -> i128 {\n        a %= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i128 @assign(i128 %0, i128 %1) {
 body:
@@ -52,10 +48,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i128>::name" = private unnamed_addr constant [11 x i8] c"core::i128\00"
-@"type_info::<core::i128>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\BDkp\09RRM\EBc\02\A0\DB47\A7\E3", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::i128>::name", i32 0, i32 0), [48 x i8] c"\80\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i128>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i16.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i16.snap
index 610d0714b..b093dc015 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i16.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i16.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign(a: i16, b: i16) -> i16 {\n        a = b;\n        a\n    }\n    pub fn assign_add(a: i16, b: i16) -> i16 {\n        a += b;\n        a\n    }\n    pub fn assign_subtract(a: i16, b: i16) -> i16 {\n        a -= b;\n        a\n    }\n    pub fn assign_multiply(a: i16, b: i16) -> i16 {\n        a *= b;\n        a\n    }\n    pub fn assign_divide(a: i16, b: i16) -> i16 {\n        a /= b;\n        a\n    }\n    pub fn assign_remainder(a: i16, b: i16) -> i16 {\n        a %= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i16 @assign(i16 %0, i16 %1) {
 body:
@@ -52,10 +48,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i16>::name" = private unnamed_addr constant [10 x i8] c"core::i16\00"
-@"type_info::<core::i16>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\05\CD|\F8Bv\D8\B1\E8\8B\8C\D8\8D\B5\89\B0", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i16>::name", i32 0, i32 0), [48 x i8] c"\10\00\00\00\02\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i16>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i32.snap
index 2914f4b72..24930243a 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign(a: i32, b: i32) -> i32 {\n        a = b;\n        a\n    }\n    pub fn assign_add(a: i32, b: i32) -> i32 {\n        a += b;\n        a\n    }\n    pub fn assign_subtract(a: i32, b: i32) -> i32 {\n        a -= b;\n        a\n    }\n    pub fn assign_multiply(a: i32, b: i32) -> i32 {\n        a *= b;\n        a\n    }\n    pub fn assign_divide(a: i32, b: i32) -> i32 {\n        a /= b;\n        a\n    }\n    pub fn assign_remainder(a: i32, b: i32) -> i32 {\n        a %= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i32 @assign(i32 %0, i32 %1) {
 body:
@@ -52,10 +48,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i64.snap
index b2678a43a..ef26621be 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign(a: i64, b: i64) -> i64 {\n        a = b;\n        a\n    }\n    pub fn assign_add(a: i64, b: i64) -> i64 {\n        a += b;\n        a\n    }\n    pub fn assign_subtract(a: i64, b: i64) -> i64 {\n        a -= b;\n        a\n    }\n    pub fn assign_multiply(a: i64, b: i64) -> i64 {\n        a *= b;\n        a\n    }\n    pub fn assign_divide(a: i64, b: i64) -> i64 {\n        a /= b;\n        a\n    }\n    pub fn assign_remainder(a: i64, b: i64) -> i64 {\n        a %= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i64 @assign(i64 %0, i64 %1) {
 body:
@@ -52,10 +48,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i64>::name" = private unnamed_addr constant [10 x i8] c"core::i64\00"
-@"type_info::<core::i64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"G\13;t\97j8\18\D7M\83`\1D\C8\19%", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i64>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i8.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i8.snap
index c82d030e7..d010bdaec 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i8.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_i8.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign(a: i8, b: i8) -> i8 {\n        a = b;\n        a\n    }\n    pub fn assign_add(a: i8, b: i8) -> i8 {\n        a += b;\n        a\n    }\n    pub fn assign_subtract(a: i8, b: i8) -> i8 {\n        a -= b;\n        a\n    }\n    pub fn assign_multiply(a: i8, b: i8) -> i8 {\n        a *= b;\n        a\n    }\n    pub fn assign_divide(a: i8, b: i8) -> i8 {\n        a /= b;\n        a\n    }\n    pub fn assign_remainder(a: i8, b: i8) -> i8 {\n        a %= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i8 @assign(i8 %0, i8 %1) {
 body:
@@ -52,10 +48,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i8>::name" = private unnamed_addr constant [9 x i8] c"core::i8\00"
-@"type_info::<core::i8>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\EF\C4\B1Z\E7\12\B1\91q\F1\0B\80U\FC\A6\0F", i8* getelementptr inbounds ([9 x i8], [9 x i8]* @"type_info::<core::i8>::name", i32 0, i32 0), [48 x i8] c"\08\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i8>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_struct.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_struct.snap
index 3e7143aa9..d38c2d587 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_struct.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_struct.snap
@@ -2,21 +2,18 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub struct(value) Value(i32, i32);\n    pub struct(gc) Heap(f64, f64);\n\n    pub fn assign_value(a: Value, b: Value) -> Value {\n        a = b;\n        a\n    }\n\n    pub fn assign_heap(a: Heap, b: Heap) -> Heap {\n        a = b;\n        a\n    }\n    // TODO: Add errors\n    // a += b;\n    // a *= b;\n    // a -= b;\n    // a /= b;\n    // a %= b;\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
 %DispatchTable = type { i8** (i8*, i8*)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 %Value = type { i32, i32 }
 %Heap = type { double, double }
 
 @allocatorHandle = external global i8*
 @dispatchTable = external global %DispatchTable
-@global_type_table = external global [7 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [27 x i64*]
 
 define %Value @assign_value(%Value %0, %Value %1) {
 body:
@@ -33,8 +30,8 @@ body:
   %deref2 = load %Value, %Value* %mem_ptr1, align 4
   %assign_value = call %Value @assign_value(%Value %deref, %Value %deref2)
   %new_ptr = load i8** (i8*, i8*)*, i8** (i8*, i8*)** getelementptr inbounds (%DispatchTable, %DispatchTable* @dispatchTable, i32 0, i32 0), align 8
-  %Value_ptr = load %"mun_codegen::ir::types::TypeInfo"*, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([7 x %"mun_codegen::ir::types::TypeInfo"*], [7 x %"mun_codegen::ir::types::TypeInfo"*]* @global_type_table, i64 0, i64 1), align 8
-  %type_info_ptr_to_i8_ptr = bitcast %"mun_codegen::ir::types::TypeInfo"* %Value_ptr to i8*
+  %Value_ptr = load i64*, i64** getelementptr inbounds ([27 x i64*], [27 x i64*]* @global_type_lookup_table, i64 0, i64 15), align 8
+  %type_info_ptr_to_i8_ptr = bitcast i64* %Value_ptr to i8*
   %allocator_handle = load i8*, i8** @allocatorHandle, align 8
   %new = call i8** %new_ptr(i8* %type_info_ptr_to_i8_ptr, i8* %allocator_handle)
   %Value_ptr_ptr = bitcast i8** %new to %Value**
@@ -53,34 +50,8 @@ body:
 source_filename = "group_name"
 
 %DispatchTable = type { i8** (i8*, i8*)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = global %DispatchTable zeroinitializer
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<Value>::name" = private unnamed_addr constant [6 x i8] c"Value\00"
-@"struct_info::<Value>::field_names.0" = private unnamed_addr constant [2 x i8] c"0\00"
-@"struct_info::<Value>::field_names.1" = private unnamed_addr constant [2 x i8] c"1\00"
-@"struct_info::<Value>::field_names" = private unnamed_addr constant [2 x i8*] [i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<Value>::field_names.0", i32 0, i32 0), i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<Value>::field_names.1", i32 0, i32 0)]
-@"struct_info::<Value>::field_types" = private unnamed_addr constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
-@"struct_info::<Value>::field_offsets" = private unnamed_addr constant [2 x i16] [i16 0, i16 4]
-@"type_info::<Value>" = private unnamed_addr constant <{ [16 x i8], i8*, [16 x i8], i8**, [0 x i8], %"mun_codegen::ir::types::TypeInfo"**, [0 x i8], i16*, [8 x i8] }> <{ [16 x i8] c"(3\1C%q\83\13+L\A6Q\F7\1DX\A6\9B", i8* getelementptr inbounds ([6 x i8], [6 x i8]* @"type_info::<Value>::name", i32 0, i32 0), [16 x i8] c"@\00\00\00\04\00\00\00\01\00\00\00\00\00\00\00", i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"struct_info::<Value>::field_names", i32 0, i32 0), [0 x i8] zeroinitializer, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([2 x %"mun_codegen::ir::types::TypeInfo"*], [2 x %"mun_codegen::ir::types::TypeInfo"*]* @"struct_info::<Value>::field_types", i32 0, i32 0), [0 x i8] zeroinitializer, i16* getelementptr inbounds ([2 x i16], [2 x i16]* @"struct_info::<Value>::field_offsets", i32 0, i32 0), [8 x i8] c"\02\00\01\00\00\00\00\00" }>
-@"type_info::<Heap>::name" = private unnamed_addr constant [5 x i8] c"Heap\00"
-@"struct_info::<Heap>::field_names.0" = private unnamed_addr constant [2 x i8] c"0\00"
-@"struct_info::<Heap>::field_names.1" = private unnamed_addr constant [2 x i8] c"1\00"
-@"struct_info::<Heap>::field_names" = private unnamed_addr constant [2 x i8*] [i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<Heap>::field_names.0", i32 0, i32 0), i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<Heap>::field_names.1", i32 0, i32 0)]
-@"type_info::<core::f64>::name" = private unnamed_addr constant [10 x i8] c"core::f64\00"
-@"type_info::<core::f64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"`\DBF\9C?YJ%G\AD4\9F\D5\92%A", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::f64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"struct_info::<Heap>::field_types" = private unnamed_addr constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f64>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f64>"]
-@"struct_info::<Heap>::field_offsets" = private unnamed_addr constant [2 x i16] [i16 0, i16 8]
-@"type_info::<Heap>" = private unnamed_addr constant <{ [16 x i8], i8*, [16 x i8], i8**, [0 x i8], %"mun_codegen::ir::types::TypeInfo"**, [0 x i8], i16*, [8 x i8] }> <{ [16 x i8] c"1\1CC\F80{\17\AFq\94\89\FB4\AC\A8\F3", i8* getelementptr inbounds ([5 x i8], [5 x i8]* @"type_info::<Heap>::name", i32 0, i32 0), [16 x i8] c"\80\00\00\00\08\00\00\00\01\00\00\00\00\00\00\00", i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"struct_info::<Heap>::field_names", i32 0, i32 0), [0 x i8] zeroinitializer, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([2 x %"mun_codegen::ir::types::TypeInfo"*], [2 x %"mun_codegen::ir::types::TypeInfo"*]* @"struct_info::<Heap>::field_types", i32 0, i32 0), [0 x i8] zeroinitializer, i16* getelementptr inbounds ([2 x i16], [2 x i16]* @"struct_info::<Heap>::field_offsets", i32 0, i32 0), [8 x i8] c"\02\00\00\00\00\00\00\00" }>
-@"type_info::<*const TypeInfo>::name" = private unnamed_addr constant [16 x i8] c"*const TypeInfo\00"
-@"type_info::<*const TypeInfo>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"=\A1-\1F\C2\A7\88`d\90\F4\B5\BEE}x", i8* getelementptr inbounds ([16 x i8], [16 x i8]* @"type_info::<*const TypeInfo>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<*const *mut core::void>::name" = private unnamed_addr constant [23 x i8] c"*const *mut core::void\00"
-@"type_info::<*const *mut core::void>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\C5fO\BD\84\DF\06\BFd+\B1\9Abv\CE\00", i8* getelementptr inbounds ([23 x i8], [23 x i8]* @"type_info::<*const *mut core::void>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<*mut core::void>::name" = private unnamed_addr constant [16 x i8] c"*mut core::void\00"
-@"type_info::<*mut core::void>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\F0Y\22\FC\95\9E\7F\CE\08T\B1\A2\CD\A7\FAz", i8* getelementptr inbounds ([16 x i8], [16 x i8]* @"type_info::<*mut core::void>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [7 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<Value>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<Heap>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*const TypeInfo>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f64>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*const *mut core::void>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*mut core::void>"]
+@global_type_lookup_table = global [27 x i64*] zeroinitializer
 @allocatorHandle = unnamed_addr global i8* null
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u128.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u128.snap
index 3c47b26cf..d30591471 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u128.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u128.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign(a: u128, b: u128) -> u128 {\n        a = b;\n        a\n    }\n    pub fn assign_add(a: u128, b: u128) -> u128 {\n        a += b;\n        a\n    }\n    pub fn assign_subtract(a: u128, b: u128) -> u128 {\n        a -= b;\n        a\n    }\n    pub fn assign_multiply(a: u128, b: u128) -> u128 {\n        a *= b;\n        a\n    }\n    pub fn assign_divide(a: u128, b: u128) -> u128 {\n        a /= b;\n        a\n    }\n    pub fn assign_remainder(a: u128, b: u128) -> u128 {\n        a %= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i128 @assign(i128 %0, i128 %1) {
 body:
@@ -52,10 +48,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u128>::name" = private unnamed_addr constant [11 x i8] c"core::u128\00"
-@"type_info::<core::u128>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\E67\1BU\E9k\95\93d\14}\1C\96S\95\F0", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::u128>::name", i32 0, i32 0), [48 x i8] c"\80\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u128>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u16.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u16.snap
index 162276751..890d75b48 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u16.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u16.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign(a: u16, b: u16) -> u16 {\n        a = b;\n        a\n    }\n    pub fn assign_add(a: u16, b: u16) -> u16 {\n        a += b;\n        a\n    }\n    pub fn assign_subtract(a: u16, b: u16) -> u16 {\n        a -= b;\n        a\n    }\n    pub fn assign_multiply(a: u16, b: u16) -> u16 {\n        a *= b;\n        a\n    }\n    pub fn assign_divide(a: u16, b: u16) -> u16 {\n        a /= b;\n        a\n    }\n    pub fn assign_remainder(a: u16, b: u16) -> u16 {\n        a %= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i16 @assign(i16 %0, i16 %1) {
 body:
@@ -52,10 +48,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u16>::name" = private unnamed_addr constant [10 x i8] c"core::u16\00"
-@"type_info::<core::u16>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"0\01\BC\BBK\E0\F2\7F&l\01\CD|q\F2\B3", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u16>::name", i32 0, i32 0), [48 x i8] c"\10\00\00\00\02\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u16>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u32.snap
index d5d190bf1..a6725d57a 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign(a: u32, b: u32) -> u32 {\n        a = b;\n        a\n    }\n    pub fn assign_add(a: u32, b: u32) -> u32 {\n        a += b;\n        a\n    }\n    pub fn assign_subtract(a: u32, b: u32) -> u32 {\n        a -= b;\n        a\n    }\n    pub fn assign_multiply(a: u32, b: u32) -> u32 {\n        a *= b;\n        a\n    }\n    pub fn assign_divide(a: u32, b: u32) -> u32 {\n        a /= b;\n        a\n    }\n    pub fn assign_remainder(a: u32, b: u32) -> u32 {\n        a %= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i32 @assign(i32 %0, i32 %1) {
 body:
@@ -52,10 +48,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u32>::name" = private unnamed_addr constant [10 x i8] c"core::u32\00"
-@"type_info::<core::u32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"daz5d\A6\BE\88\81=&Y\A1+\C6\1D", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u32>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u64.snap
index c2b95a918..9ce0751b8 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign(a: u64, b: u64) -> u64 {\n        a = b;\n        a\n    }\n    pub fn assign_add(a: u64, b: u64) -> u64 {\n        a += b;\n        a\n    }\n    pub fn assign_subtract(a: u64, b: u64) -> u64 {\n        a -= b;\n        a\n    }\n    pub fn assign_multiply(a: u64, b: u64) -> u64 {\n        a *= b;\n        a\n    }\n    pub fn assign_divide(a: u64, b: u64) -> u64 {\n        a /= b;\n        a\n    }\n    pub fn assign_remainder(a: u64, b: u64) -> u64 {\n        a %= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i64 @assign(i64 %0, i64 %1) {
 body:
@@ -52,10 +48,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u64>::name" = private unnamed_addr constant [10 x i8] c"core::u64\00"
-@"type_info::<core::u64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\A6\E7g \D1\8B\1Aq`\1F\1E\07\BB5@q", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u64>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u8.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u8.snap
index ae6366ecf..7aa6ed951 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u8.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__assignment_op_u8.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn assign(a: u8, b: u8) -> u8 {\n        a = b;\n        a\n    }\n    pub fn assign_add(a: u8, b: u8) -> u8 {\n        a += b;\n        a\n    }\n    pub fn assign_subtract(a: u8, b: u8) -> u8 {\n        a -= b;\n        a\n    }\n    pub fn assign_multiply(a: u8, b: u8) -> u8 {\n        a *= b;\n        a\n    }\n    pub fn assign_divide(a: u8, b: u8) -> u8 {\n        a /= b;\n        a\n    }\n    pub fn assign_remainder(a: u8, b: u8) -> u8 {\n        a %= b;\n        a\n    }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i8 @assign(i8 %0, i8 %1) {
 body:
@@ -52,10 +48,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u8>::name" = private unnamed_addr constant [9 x i8] c"core::u8\00"
-@"type_info::<core::u8>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\A0y\A7S\B6(n\F7f&H\E1\F9\AD\04>", i8* getelementptr inbounds ([9 x i8], [9 x i8]* @"type_info::<core::u8>::name", i32 0, i32 0), [48 x i8] c"\08\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u8>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_bool.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_bool.snap
index ada3dcf66..9ac570d16 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_bool.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_bool.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn not(a: bool) -> bool { !a }\n    pub fn bitand(a: bool, b: bool) -> bool { a & b }\n    pub fn bitor(a: bool, b: bool) -> bool { a | b }\n    pub fn bitxor(a: bool, b: bool) -> bool { a ^ b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [11 x i64*]
 
 define i1 @not(i1 %0) {
 body:
@@ -41,10 +37,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>"]
+@global_type_lookup_table = global [11 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i128.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i128.snap
index 64088a422..fb62e91cc 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i128.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i128.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn not(a: i128) -> i128 { !a }\n    pub fn bitand(a: i128, b: i128) -> i128 { a & b }\n    pub fn bitor(a: i128, b: i128) -> i128 { a | b }\n    pub fn bitxor(a: i128, b: i128) -> i128 { a ^ b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [11 x i64*]
 
 define i128 @not(i128 %0) {
 body:
@@ -41,10 +37,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i128>::name" = private unnamed_addr constant [11 x i8] c"core::i128\00"
-@"type_info::<core::i128>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\BDkp\09RRM\EBc\02\A0\DB47\A7\E3", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::i128>::name", i32 0, i32 0), [48 x i8] c"\80\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i128>"]
+@global_type_lookup_table = global [11 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i16.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i16.snap
index 3c6b04722..c4d547e01 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i16.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i16.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn not(a: i16) -> i16 { !a }\n    pub fn bitand(a: i16, b: i16) -> i16 { a & b }\n    pub fn bitor(a: i16, b: i16) -> i16 { a | b }\n    pub fn bitxor(a: i16, b: i16) -> i16 { a ^ b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [11 x i64*]
 
 define i16 @not(i16 %0) {
 body:
@@ -41,10 +37,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i16>::name" = private unnamed_addr constant [10 x i8] c"core::i16\00"
-@"type_info::<core::i16>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\05\CD|\F8Bv\D8\B1\E8\8B\8C\D8\8D\B5\89\B0", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i16>::name", i32 0, i32 0), [48 x i8] c"\10\00\00\00\02\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i16>"]
+@global_type_lookup_table = global [11 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i32.snap
index 4fe1654b1..b414eb6dc 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn not(a: i32) -> i32 { !a }\n    pub fn bitand(a: i32, b: i32) -> i32 { a & b }\n    pub fn bitor(a: i32, b: i32) -> i32 { a | b }\n    pub fn bitxor(a: i32, b: i32) -> i32 { a ^ b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [11 x i64*]
 
 define i32 @not(i32 %0) {
 body:
@@ -41,10 +37,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [11 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i64.snap
index a287c01fa..790f9ff2e 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn not(a: i64) -> i64 { !a }\n    pub fn bitand(a: i64, b: i64) -> i64 { a & b }\n    pub fn bitor(a: i64, b: i64) -> i64 { a | b }\n    pub fn bitxor(a: i64, b: i64) -> i64 { a ^ b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [11 x i64*]
 
 define i64 @not(i64 %0) {
 body:
@@ -41,10 +37,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i64>::name" = private unnamed_addr constant [10 x i8] c"core::i64\00"
-@"type_info::<core::i64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"G\13;t\97j8\18\D7M\83`\1D\C8\19%", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i64>"]
+@global_type_lookup_table = global [11 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i8.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i8.snap
index 655f71d9e..e5fa2f647 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i8.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_i8.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn not(a: i8) -> i8 { !a }\n    pub fn bitand(a: i8, b: i8) -> i8 { a & b }\n    pub fn bitor(a: i8, b: i8) -> i8 { a | b }\n    pub fn bitxor(a: i8, b: i8) -> i8 { a ^ b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [11 x i64*]
 
 define i8 @not(i8 %0) {
 body:
@@ -41,10 +37,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i8>::name" = private unnamed_addr constant [9 x i8] c"core::i8\00"
-@"type_info::<core::i8>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\EF\C4\B1Z\E7\12\B1\91q\F1\0B\80U\FC\A6\0F", i8* getelementptr inbounds ([9 x i8], [9 x i8]* @"type_info::<core::i8>::name", i32 0, i32 0), [48 x i8] c"\08\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i8>"]
+@global_type_lookup_table = global [11 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u128.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u128.snap
index 4ed9c7cec..7238bc558 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u128.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u128.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn not(a: u128) -> u128 { !a }\n    pub fn bitand(a: u128, b: u128) -> u128 { a & b }\n    pub fn bitor(a: u128, b: u128) -> u128 { a | b }\n    pub fn bitxor(a: u128, b: u128) -> u128 { a ^ b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [11 x i64*]
 
 define i128 @not(i128 %0) {
 body:
@@ -41,10 +37,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u128>::name" = private unnamed_addr constant [11 x i8] c"core::u128\00"
-@"type_info::<core::u128>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\E67\1BU\E9k\95\93d\14}\1C\96S\95\F0", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::u128>::name", i32 0, i32 0), [48 x i8] c"\80\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u128>"]
+@global_type_lookup_table = global [11 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u16.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u16.snap
index ffa694dc1..bf0b4e9f2 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u16.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u16.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn not(a: u16) -> u16 { !a }\n    pub fn bitand(a: u16, b: u16) -> u16 { a & b }\n    pub fn bitor(a: u16, b: u16) -> u16 { a | b }\n    pub fn bitxor(a: u16, b: u16) -> u16 { a ^ b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [11 x i64*]
 
 define i16 @not(i16 %0) {
 body:
@@ -41,10 +37,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u16>::name" = private unnamed_addr constant [10 x i8] c"core::u16\00"
-@"type_info::<core::u16>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"0\01\BC\BBK\E0\F2\7F&l\01\CD|q\F2\B3", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u16>::name", i32 0, i32 0), [48 x i8] c"\10\00\00\00\02\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u16>"]
+@global_type_lookup_table = global [11 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u32.snap
index f425be5d4..ed88c1e5e 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn not(a: u32) -> u32 { !a }\n    pub fn bitand(a: u32, b: u32) -> u32 { a & b }\n    pub fn bitor(a: u32, b: u32) -> u32 { a | b }\n    pub fn bitxor(a: u32, b: u32) -> u32 { a ^ b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [11 x i64*]
 
 define i32 @not(i32 %0) {
 body:
@@ -41,10 +37,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u32>::name" = private unnamed_addr constant [10 x i8] c"core::u32\00"
-@"type_info::<core::u32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"daz5d\A6\BE\88\81=&Y\A1+\C6\1D", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u32>"]
+@global_type_lookup_table = global [11 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u64.snap
index 624b89edf..0f624f4ea 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn not(a: u64) -> u64 { !a }\n    pub fn bitand(a: u64, b: u64) -> u64 { a & b }\n    pub fn bitor(a: u64, b: u64) -> u64 { a | b }\n    pub fn bitxor(a: u64, b: u64) -> u64 { a ^ b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [11 x i64*]
 
 define i64 @not(i64 %0) {
 body:
@@ -41,10 +37,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u64>::name" = private unnamed_addr constant [10 x i8] c"core::u64\00"
-@"type_info::<core::u64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\A6\E7g \D1\8B\1Aq`\1F\1E\07\BB5@q", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u64>"]
+@global_type_lookup_table = global [11 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u8.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u8.snap
index 071c0308f..1b44b278e 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u8.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__bit_op_u8.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn not(a: u8) -> u8 { !a }\n    pub fn bitand(a: u8, b: u8) -> u8 { a & b }\n    pub fn bitor(a: u8, b: u8) -> u8 { a | b }\n    pub fn bitxor(a: u8, b: u8) -> u8 { a ^ b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [11 x i64*]
 
 define i8 @not(i8 %0) {
 body:
@@ -41,10 +37,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u8>::name" = private unnamed_addr constant [9 x i8] c"core::u8\00"
-@"type_info::<core::u8>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\A0y\A7S\B6(n\F7f&H\E1\F9\AD\04>", i8* getelementptr inbounds ([9 x i8], [9 x i8]* @"type_info::<core::u8>::name", i32 0, i32 0), [48 x i8] c"\08\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u8>"]
+@global_type_lookup_table = global [11 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_bool.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_bool.snap
index 6a61c2094..a08ea5198 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_bool.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_bool.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn equals(a: bool, b: bool) -> bool { a == b }\n    pub fn not_equal(a: bool, b: bool) -> bool { a != b}\n    pub fn less(a: bool, b: bool) -> bool { a < b }\n    pub fn less_equal(a: bool, b: bool) -> bool { a <= b }\n    pub fn greater(a: bool, b: bool) -> bool { a > b }\n    pub fn greater_equal(a: bool, b: bool) -> bool { a >= b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i1 @equals(i1 %0, i1 %1) {
 body:
@@ -53,10 +49,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_f32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_f32.snap
index 8473fa42b..72611337c 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_f32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_f32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn equals(a: f32, b: f32) -> bool { a == b }\n    pub fn not_equal(a: f32, b: f32) -> bool { a != b}\n    pub fn less(a: f32, b: f32) -> bool { a < b }\n    pub fn less_equal(a: f32, b: f32) -> bool { a <= b }\n    pub fn greater(a: f32, b: f32) -> bool { a > b }\n    pub fn greater_equal(a: f32, b: f32) -> bool { a >= b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [2 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i1 @equals(float %0, float %1) {
 body:
@@ -53,12 +49,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::f32>::name" = private unnamed_addr constant [10 x i8] c"core::f32\00"
-@"type_info::<core::f32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"P\19b7\A8k\F2\81P\FB\83\F5P\B0\82!", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::f32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f32>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_f64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_f64.snap
index dd460bd20..a281ed81b 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_f64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_f64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn equals(a: f64, b: f64) -> bool { a == b }\n    pub fn not_equal(a: f64, b: f64) -> bool { a != b}\n    pub fn less(a: f64, b: f64) -> bool { a < b }\n    pub fn less_equal(a: f64, b: f64) -> bool { a <= b }\n    pub fn greater(a: f64, b: f64) -> bool { a > b }\n    pub fn greater_equal(a: f64, b: f64) -> bool { a >= b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [2 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i1 @equals(double %0, double %1) {
 body:
@@ -53,12 +49,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::f64>::name" = private unnamed_addr constant [10 x i8] c"core::f64\00"
-@"type_info::<core::f64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"`\DBF\9C?YJ%G\AD4\9F\D5\92%A", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::f64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f64>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i128.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i128.snap
index 4e9d7dd1f..7904205a5 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i128.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i128.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn equals(a: i128, b: i128) -> bool { a == b }\n    pub fn not_equal(a: i128, b: i128) -> bool { a != b}\n    pub fn less(a: i128, b: i128) -> bool { a < b }\n    pub fn less_equal(a: i128, b: i128) -> bool { a <= b }\n    pub fn greater(a: i128, b: i128) -> bool { a > b }\n    pub fn greater_equal(a: i128, b: i128) -> bool { a >= b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [2 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i1 @equals(i128 %0, i128 %1) {
 body:
@@ -53,12 +49,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<core::i128>::name" = private unnamed_addr constant [11 x i8] c"core::i128\00"
-@"type_info::<core::i128>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\BDkp\09RRM\EBc\02\A0\DB47\A7\E3", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::i128>::name", i32 0, i32 0), [48 x i8] c"\80\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i128>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i16.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i16.snap
index 22939a104..c2729c687 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i16.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i16.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn equals(a: i16, b: i16) -> bool { a == b }\n    pub fn not_equal(a: i16, b: i16) -> bool { a != b}\n    pub fn less(a: i16, b: i16) -> bool { a < b }\n    pub fn less_equal(a: i16, b: i16) -> bool { a <= b }\n    pub fn greater(a: i16, b: i16) -> bool { a > b }\n    pub fn greater_equal(a: i16, b: i16) -> bool { a >= b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [2 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i1 @equals(i16 %0, i16 %1) {
 body:
@@ -53,12 +49,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i16>::name" = private unnamed_addr constant [10 x i8] c"core::i16\00"
-@"type_info::<core::i16>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\05\CD|\F8Bv\D8\B1\E8\8B\8C\D8\8D\B5\89\B0", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i16>::name", i32 0, i32 0), [48 x i8] c"\10\00\00\00\02\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i16>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i32.snap
index 82c61d69f..3763b9168 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn equals(a: i32, b: i32) -> bool { a == b }\n    pub fn not_equal(a: i32, b: i32) -> bool { a != b}\n    pub fn less(a: i32, b: i32) -> bool { a < b }\n    pub fn less_equal(a: i32, b: i32) -> bool { a <= b }\n    pub fn greater(a: i32, b: i32) -> bool { a > b }\n    pub fn greater_equal(a: i32, b: i32) -> bool { a >= b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [2 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i1 @equals(i32 %0, i32 %1) {
 body:
@@ -53,12 +49,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i64.snap
index e531dc156..058a71125 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn equals(a: i64, b: i64) -> bool { a == b }\n    pub fn not_equal(a: i64, b: i64) -> bool { a != b}\n    pub fn less(a: i64, b: i64) -> bool { a < b }\n    pub fn less_equal(a: i64, b: i64) -> bool { a <= b }\n    pub fn greater(a: i64, b: i64) -> bool { a > b }\n    pub fn greater_equal(a: i64, b: i64) -> bool { a >= b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [2 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i1 @equals(i64 %0, i64 %1) {
 body:
@@ -53,12 +49,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i64>::name" = private unnamed_addr constant [10 x i8] c"core::i64\00"
-@"type_info::<core::i64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"G\13;t\97j8\18\D7M\83`\1D\C8\19%", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i64>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i8.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i8.snap
index 112cd385a..6a00b722a 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i8.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_i8.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn equals(a: i8, b: i8) -> bool { a == b }\n    pub fn not_equal(a: i8, b: i8) -> bool { a != b}\n    pub fn less(a: i8, b: i8) -> bool { a < b }\n    pub fn less_equal(a: i8, b: i8) -> bool { a <= b }\n    pub fn greater(a: i8, b: i8) -> bool { a > b }\n    pub fn greater_equal(a: i8, b: i8) -> bool { a >= b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [2 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i1 @equals(i8 %0, i8 %1) {
 body:
@@ -53,12 +49,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<core::i8>::name" = private unnamed_addr constant [9 x i8] c"core::i8\00"
-@"type_info::<core::i8>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\EF\C4\B1Z\E7\12\B1\91q\F1\0B\80U\FC\A6\0F", i8* getelementptr inbounds ([9 x i8], [9 x i8]* @"type_info::<core::i8>::name", i32 0, i32 0), [48 x i8] c"\08\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i8>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u128.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u128.snap
index 6fd77d1bb..8f2938c19 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u128.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u128.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn equals(a: u128, b: u128) -> bool { a == b }\n    pub fn not_equal(a: u128, b: u128) -> bool { a != b}\n    pub fn less(a: u128, b: u128) -> bool { a < b }\n    pub fn less_equal(a: u128, b: u128) -> bool { a <= b }\n    pub fn greater(a: u128, b: u128) -> bool { a > b }\n    pub fn greater_equal(a: u128, b: u128) -> bool { a >= b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [2 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i1 @equals(i128 %0, i128 %1) {
 body:
@@ -53,12 +49,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<core::u128>::name" = private unnamed_addr constant [11 x i8] c"core::u128\00"
-@"type_info::<core::u128>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\E67\1BU\E9k\95\93d\14}\1C\96S\95\F0", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::u128>::name", i32 0, i32 0), [48 x i8] c"\80\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u128>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u16.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u16.snap
index 161e09819..f2d5089f1 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u16.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u16.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn equals(a: u16, b: u16) -> bool { a == b }\n    pub fn not_equal(a: u16, b: u16) -> bool { a != b}\n    pub fn less(a: u16, b: u16) -> bool { a < b }\n    pub fn less_equal(a: u16, b: u16) -> bool { a <= b }\n    pub fn greater(a: u16, b: u16) -> bool { a > b }\n    pub fn greater_equal(a: u16, b: u16) -> bool { a >= b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [2 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i1 @equals(i16 %0, i16 %1) {
 body:
@@ -53,12 +49,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u16>::name" = private unnamed_addr constant [10 x i8] c"core::u16\00"
-@"type_info::<core::u16>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"0\01\BC\BBK\E0\F2\7F&l\01\CD|q\F2\B3", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u16>::name", i32 0, i32 0), [48 x i8] c"\10\00\00\00\02\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u16>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u32.snap
index fff4791bc..3d6496caf 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn equals(a: u32, b: u32) -> bool { a == b }\n    pub fn not_equal(a: u32, b: u32) -> bool { a != b}\n    pub fn less(a: u32, b: u32) -> bool { a < b }\n    pub fn less_equal(a: u32, b: u32) -> bool { a <= b }\n    pub fn greater(a: u32, b: u32) -> bool { a > b }\n    pub fn greater_equal(a: u32, b: u32) -> bool { a >= b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [2 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i1 @equals(i32 %0, i32 %1) {
 body:
@@ -53,12 +49,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u32>::name" = private unnamed_addr constant [10 x i8] c"core::u32\00"
-@"type_info::<core::u32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"daz5d\A6\BE\88\81=&Y\A1+\C6\1D", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u32>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u64.snap
index 0654fab92..df8462670 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn equals(a: u64, b: u64) -> bool { a == b }\n    pub fn not_equal(a: u64, b: u64) -> bool { a != b}\n    pub fn less(a: u64, b: u64) -> bool { a < b }\n    pub fn less_equal(a: u64, b: u64) -> bool { a <= b }\n    pub fn greater(a: u64, b: u64) -> bool { a > b }\n    pub fn greater_equal(a: u64, b: u64) -> bool { a >= b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [2 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i1 @equals(i64 %0, i64 %1) {
 body:
@@ -53,12 +49,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<core::u64>::name" = private unnamed_addr constant [10 x i8] c"core::u64\00"
-@"type_info::<core::u64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\A6\E7g \D1\8B\1Aq`\1F\1E\07\BB5@q", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u64>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u8.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u8.snap
index d53df092c..ec1bd7398 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u8.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__compare_op_u8.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn equals(a: u8, b: u8) -> bool { a == b }\n    pub fn not_equal(a: u8, b: u8) -> bool { a != b}\n    pub fn less(a: u8, b: u8) -> bool { a < b }\n    pub fn less_equal(a: u8, b: u8) -> bool { a <= b }\n    pub fn greater(a: u8, b: u8) -> bool { a > b }\n    pub fn greater_equal(a: u8, b: u8) -> bool { a >= b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [2 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [18 x i64*]
 
 define i1 @equals(i8 %0, i8 %1) {
 body:
@@ -53,12 +49,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<core::u8>::name" = private unnamed_addr constant [9 x i8] c"core::u8\00"
-@"type_info::<core::u8>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\A0y\A7S\B6(n\F7f&H\E1\F9\AD\04>", i8* getelementptr inbounds ([9 x i8], [9 x i8]* @"type_info::<core::u8>::name", i32 0, i32 0), [48 x i8] c"\08\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u8>"]
+@global_type_lookup_table = global [18 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__conditional_return_expr.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__conditional_return_expr.snap
index 4d6d1be2f..126b1bf29 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__conditional_return_expr.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__conditional_return_expr.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn main(a:i32) -> i32 {\n        if a > 4 {\n            return a;\n        }\n        a - 1\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i32 @main(i32 %0) {
 body:
@@ -30,10 +26,5 @@ if_merge:                                         ; preds = %body
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__extern_fn.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__extern_fn.snap
index dcdde0f45..6ffda8b04 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__extern_fn.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__extern_fn.snap
@@ -2,18 +2,15 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    extern fn add(a:i32, b:i32) -> i32;\n    pub fn main() {\n        add(3,4);\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
 %DispatchTable = type { i32 (i32, i32)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = external global %DispatchTable
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define void @main() {
 body:
@@ -27,11 +24,7 @@ body:
 source_filename = "group_name"
 
 %DispatchTable = type { i32 (i32, i32)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = global %DispatchTable zeroinitializer
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__fibonacci.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__fibonacci.snap
index 3bcdab7bc..d6a25314f 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__fibonacci.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__fibonacci.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn fibonacci(n:i32) -> i32 {\n        if n <= 1 {\n            n\n        } else {\n            fibonacci(n-1) + fibonacci(n-2)\n        }\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i32 @fibonacci(i32 %0) {
 body:
@@ -38,10 +34,5 @@ if_merge:                                         ; preds = %else, %then
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__fibonacci_loop.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__fibonacci_loop.snap
index 10b82f06d..aeaecb60a 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__fibonacci_loop.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__fibonacci_loop.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn fibonacci(n:i32) -> i32 {\n        let a = 0;\n        let b = 1;\n        let i = 1;\n        loop {\n            if i > n {\n                return a\n            }\n            let sum = a + b;\n            a = b;\n            b = sum;\n            i += 1;\n        }\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i32 @fibonacci(i32 %0) {
 body:
@@ -37,10 +33,5 @@ if_merge:                                         ; preds = %loop
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__field_crash.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__field_crash.snap
index cfa1dde6f..924871838 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__field_crash.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__field_crash.snap
@@ -2,20 +2,17 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    struct(gc) Foo { a: i32 };\n\n    pub fn main(c:i32) -> i32 {\n        let b = Foo { a: c + 5 }\n        b.a\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
 %DispatchTable = type { i8** (i8*, i8*)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 %Foo = type { i32 }
 
 @allocatorHandle = external global i8*
 @dispatchTable = external global %DispatchTable
-@global_type_table = external global [5 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [7 x i64*]
 
 define i32 @main(i32 %0) {
 body:
@@ -26,8 +23,8 @@ body:
   %add = add i32 %c1, 5
   %init = insertvalue %Foo undef, i32 %add, 0
   %new_ptr = load i8** (i8*, i8*)*, i8** (i8*, i8*)** getelementptr inbounds (%DispatchTable, %DispatchTable* @dispatchTable, i32 0, i32 0), align 8
-  %Foo_ptr = load %"mun_codegen::ir::types::TypeInfo"*, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([5 x %"mun_codegen::ir::types::TypeInfo"*], [5 x %"mun_codegen::ir::types::TypeInfo"*]* @global_type_table, i64 0, i64 1), align 8
-  %type_info_ptr_to_i8_ptr = bitcast %"mun_codegen::ir::types::TypeInfo"* %Foo_ptr to i8*
+  %Foo_ptr = load i64*, i64** getelementptr inbounds ([7 x i64*], [7 x i64*]* @global_type_lookup_table, i64 0, i64 3), align 8
+  %type_info_ptr_to_i8_ptr = bitcast i64* %Foo_ptr to i8*
   %allocator_handle = load i8*, i8** @allocatorHandle, align 8
   %new = call i8** %new_ptr(i8* %type_info_ptr_to_i8_ptr, i8* %allocator_handle)
   %Foo_ptr_ptr = bitcast i8** %new to %Foo**
@@ -46,24 +43,8 @@ body:
 source_filename = "group_name"
 
 %DispatchTable = type { i8** (i8*, i8*)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = global %DispatchTable zeroinitializer
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<Foo>::name" = private unnamed_addr constant [4 x i8] c"Foo\00"
-@"struct_info::<Foo>::field_names.0" = private unnamed_addr constant [2 x i8] c"a\00"
-@"struct_info::<Foo>::field_names" = private unnamed_addr constant [1 x i8*] [i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<Foo>::field_names.0", i32 0, i32 0)]
-@"struct_info::<Foo>::field_types" = private unnamed_addr constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
-@"struct_info::<Foo>::field_offsets" = private unnamed_addr constant [1 x i16] zeroinitializer
-@"type_info::<Foo>" = private unnamed_addr constant <{ [16 x i8], i8*, [16 x i8], i8**, [0 x i8], %"mun_codegen::ir::types::TypeInfo"**, [0 x i8], i16*, [8 x i8] }> <{ [16 x i8] c"8\AD\C9\D9Y\D2\AA<ME}\CD.\A5\A0\DB", i8* getelementptr inbounds ([4 x i8], [4 x i8]* @"type_info::<Foo>::name", i32 0, i32 0), [16 x i8] c" \00\00\00\04\00\00\00\01\00\00\00\00\00\00\00", i8** getelementptr inbounds ([1 x i8*], [1 x i8*]* @"struct_info::<Foo>::field_names", i32 0, i32 0), [0 x i8] zeroinitializer, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([1 x %"mun_codegen::ir::types::TypeInfo"*], [1 x %"mun_codegen::ir::types::TypeInfo"*]* @"struct_info::<Foo>::field_types", i32 0, i32 0), [0 x i8] zeroinitializer, i16* getelementptr inbounds ([1 x i16], [1 x i16]* @"struct_info::<Foo>::field_offsets", i32 0, i32 0), [8 x i8] c"\01\00\00\00\00\00\00\00" }>
-@"type_info::<*const TypeInfo>::name" = private unnamed_addr constant [16 x i8] c"*const TypeInfo\00"
-@"type_info::<*const TypeInfo>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"=\A1-\1F\C2\A7\88`d\90\F4\B5\BEE}x", i8* getelementptr inbounds ([16 x i8], [16 x i8]* @"type_info::<*const TypeInfo>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<*const *mut core::void>::name" = private unnamed_addr constant [23 x i8] c"*const *mut core::void\00"
-@"type_info::<*const *mut core::void>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\C5fO\BD\84\DF\06\BFd+\B1\9Abv\CE\00", i8* getelementptr inbounds ([23 x i8], [23 x i8]* @"type_info::<*const *mut core::void>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<*mut core::void>::name" = private unnamed_addr constant [16 x i8] c"*mut core::void\00"
-@"type_info::<*mut core::void>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\F0Y\22\FC\95\9E\7F\CE\08T\B1\A2\CD\A7\FAz", i8* getelementptr inbounds ([16 x i8], [16 x i8]* @"type_info::<*mut core::void>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [5 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<Foo>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*const TypeInfo>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*const *mut core::void>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*mut core::void>"]
+@global_type_lookup_table = global [7 x i64*] zeroinitializer
 @allocatorHandle = unnamed_addr global i8* null
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__field_expr.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__field_expr.snap
index bdda85bef..21e109ead 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__field_expr.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__field_expr.snap
@@ -2,21 +2,18 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub struct(value) Bar(f64, Foo);\n    pub struct(value) Foo { a: i32 };\n\n    fn bar_1(bar: Bar) -> Foo {\n        bar.1\n    }\n\n    fn foo_a(foo: Foo) -> i32 {\n        foo.a\n    }\n\n    pub fn bar_1_foo_a(bar: Bar) -> i32 {\n        foo_a(bar_1(bar))\n    }\n\n    pub fn main() -> i32 {\n        let a: Foo = Foo { a: 5 };\n        let b: Bar = Bar(1.23, a);\n        let aa_lhs = a.a + 2;\n        let aa_rhs = 2 + a.a;\n        aa_lhs + aa_rhs\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
 %DispatchTable = type { i8** (i8*, i8*)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 %Foo = type { i32 }
 %Bar = type { double, %Foo }
 
 @allocatorHandle = external global i8*
 @dispatchTable = external global %DispatchTable
-@global_type_table = external global [7 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [15 x i64*]
 
 define %Foo @bar_1(%Bar %0) {
 body:
@@ -58,33 +55,8 @@ body:
 source_filename = "group_name"
 
 %DispatchTable = type { i8** (i8*, i8*)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = global %DispatchTable zeroinitializer
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<Foo>::name" = private unnamed_addr constant [4 x i8] c"Foo\00"
-@"struct_info::<Foo>::field_names.0" = private unnamed_addr constant [2 x i8] c"a\00"
-@"struct_info::<Foo>::field_names" = private unnamed_addr constant [1 x i8*] [i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<Foo>::field_names.0", i32 0, i32 0)]
-@"struct_info::<Foo>::field_types" = private unnamed_addr constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
-@"struct_info::<Foo>::field_offsets" = private unnamed_addr constant [1 x i16] zeroinitializer
-@"type_info::<Foo>" = private unnamed_addr constant <{ [16 x i8], i8*, [16 x i8], i8**, [0 x i8], %"mun_codegen::ir::types::TypeInfo"**, [0 x i8], i16*, [8 x i8] }> <{ [16 x i8] c"8\AD\C9\D9Y\D2\AA<ME}\CD.\A5\A0\DB", i8* getelementptr inbounds ([4 x i8], [4 x i8]* @"type_info::<Foo>::name", i32 0, i32 0), [16 x i8] c" \00\00\00\04\00\00\00\01\00\00\00\00\00\00\00", i8** getelementptr inbounds ([1 x i8*], [1 x i8*]* @"struct_info::<Foo>::field_names", i32 0, i32 0), [0 x i8] zeroinitializer, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([1 x %"mun_codegen::ir::types::TypeInfo"*], [1 x %"mun_codegen::ir::types::TypeInfo"*]* @"struct_info::<Foo>::field_types", i32 0, i32 0), [0 x i8] zeroinitializer, i16* getelementptr inbounds ([1 x i16], [1 x i16]* @"struct_info::<Foo>::field_offsets", i32 0, i32 0), [8 x i8] c"\01\00\01\00\00\00\00\00" }>
-@"type_info::<*const TypeInfo>::name" = private unnamed_addr constant [16 x i8] c"*const TypeInfo\00"
-@"type_info::<*const TypeInfo>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"=\A1-\1F\C2\A7\88`d\90\F4\B5\BEE}x", i8* getelementptr inbounds ([16 x i8], [16 x i8]* @"type_info::<*const TypeInfo>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<core::f64>::name" = private unnamed_addr constant [10 x i8] c"core::f64\00"
-@"type_info::<core::f64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"`\DBF\9C?YJ%G\AD4\9F\D5\92%A", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::f64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<*const *mut core::void>::name" = private unnamed_addr constant [23 x i8] c"*const *mut core::void\00"
-@"type_info::<*const *mut core::void>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\C5fO\BD\84\DF\06\BFd+\B1\9Abv\CE\00", i8* getelementptr inbounds ([23 x i8], [23 x i8]* @"type_info::<*const *mut core::void>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<*mut core::void>::name" = private unnamed_addr constant [16 x i8] c"*mut core::void\00"
-@"type_info::<*mut core::void>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\F0Y\22\FC\95\9E\7F\CE\08T\B1\A2\CD\A7\FAz", i8* getelementptr inbounds ([16 x i8], [16 x i8]* @"type_info::<*mut core::void>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<Bar>::name" = private unnamed_addr constant [4 x i8] c"Bar\00"
-@"struct_info::<Bar>::field_names.0" = private unnamed_addr constant [2 x i8] c"0\00"
-@"struct_info::<Bar>::field_names.1" = private unnamed_addr constant [2 x i8] c"1\00"
-@"struct_info::<Bar>::field_names" = private unnamed_addr constant [2 x i8*] [i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<Bar>::field_names.0", i32 0, i32 0), i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<Bar>::field_names.1", i32 0, i32 0)]
-@"struct_info::<Bar>::field_types" = private unnamed_addr constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f64>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<Foo>"]
-@"struct_info::<Bar>::field_offsets" = private unnamed_addr constant [2 x i16] [i16 0, i16 8]
-@"type_info::<Bar>" = private unnamed_addr constant <{ [16 x i8], i8*, [16 x i8], i8**, [0 x i8], %"mun_codegen::ir::types::TypeInfo"**, [0 x i8], i16*, [8 x i8] }> <{ [16 x i8] c"\FC8#Lvd)F\B1Q\06\8B\02pl\10", i8* getelementptr inbounds ([4 x i8], [4 x i8]* @"type_info::<Bar>::name", i32 0, i32 0), [16 x i8] c"\80\00\00\00\08\00\00\00\01\00\00\00\00\00\00\00", i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"struct_info::<Bar>::field_names", i32 0, i32 0), [0 x i8] zeroinitializer, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([2 x %"mun_codegen::ir::types::TypeInfo"*], [2 x %"mun_codegen::ir::types::TypeInfo"*]* @"struct_info::<Bar>::field_types", i32 0, i32 0), [0 x i8] zeroinitializer, i16* getelementptr inbounds ([2 x i16], [2 x i16]* @"struct_info::<Bar>::field_offsets", i32 0, i32 0), [8 x i8] c"\02\00\01\00\00\00\00\00" }>
-@global_type_table = constant [7 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<Foo>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*const TypeInfo>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f64>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*const *mut core::void>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*mut core::void>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<Bar>"]
+@global_type_lookup_table = global [15 x i64*] zeroinitializer
 @allocatorHandle = unnamed_addr global i8* null
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__function_arguments.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__function_arguments.snap
index 87d237dae..a900b479a 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__function_arguments.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__function_arguments.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn main(a:i32) -> i32 {\n      a\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i32 @main(i32 %0) {
 body:
@@ -22,10 +18,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__function_calls.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__function_calls.snap
index 0cd026e23..d85f85f3a 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__function_calls.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__function_calls.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    fn add_impl(a:i32, b:i32) -> i32 {\n        a+b\n    }\n\n    fn add(a:i32, b:i32) -> i32 {\n      add_impl(a,b)\n    }\n\n    pub fn test() -> i32 {\n      add(4,5)\n      add_impl(4,5)\n      add(4,5)\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [1 x i64*]
 
 define i32 @add_impl(i32 %0, i32 %1) {
 body:
@@ -37,10 +33,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [1 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__gc_struct.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__gc_struct.snap
index 63c0ff38a..00fff70df 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__gc_struct.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__gc_struct.snap
@@ -2,28 +2,25 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    struct(gc) Foo { a: i32, b: i32 };\n\n    pub fn foo() {\n        let a = Foo { a: 3, b: 4 };\n        a.b += 3;\n        let b = a;\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
 %DispatchTable = type { i8** (i8*, i8*)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 %Foo = type { i32, i32 }
 
 @allocatorHandle = external global i8*
 @dispatchTable = external global %DispatchTable
-@global_type_table = external global [5 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define void @foo() {
 body:
   %b = alloca %Foo**, align 8
   %a = alloca %Foo**, align 8
   %new_ptr = load i8** (i8*, i8*)*, i8** (i8*, i8*)** getelementptr inbounds (%DispatchTable, %DispatchTable* @dispatchTable, i32 0, i32 0), align 8
-  %Foo_ptr = load %"mun_codegen::ir::types::TypeInfo"*, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([5 x %"mun_codegen::ir::types::TypeInfo"*], [5 x %"mun_codegen::ir::types::TypeInfo"*]* @global_type_table, i64 0, i64 0), align 8
-  %type_info_ptr_to_i8_ptr = bitcast %"mun_codegen::ir::types::TypeInfo"* %Foo_ptr to i8*
+  %Foo_ptr = load i64*, i64** getelementptr inbounds ([6 x i64*], [6 x i64*]* @global_type_lookup_table, i64 0, i64 3), align 8
+  %type_info_ptr_to_i8_ptr = bitcast i64* %Foo_ptr to i8*
   %allocator_handle = load i8*, i8** @allocatorHandle, align 8
   %new = call i8** %new_ptr(i8* %type_info_ptr_to_i8_ptr, i8* %allocator_handle)
   %Foo_ptr_ptr = bitcast i8** %new to %Foo**
@@ -49,25 +46,8 @@ body:
 source_filename = "group_name"
 
 %DispatchTable = type { i8** (i8*, i8*)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = global %DispatchTable zeroinitializer
-@"type_info::<Foo>::name" = private unnamed_addr constant [4 x i8] c"Foo\00"
-@"struct_info::<Foo>::field_names.0" = private unnamed_addr constant [2 x i8] c"a\00"
-@"struct_info::<Foo>::field_names.1" = private unnamed_addr constant [2 x i8] c"b\00"
-@"struct_info::<Foo>::field_names" = private unnamed_addr constant [2 x i8*] [i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<Foo>::field_names.0", i32 0, i32 0), i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<Foo>::field_names.1", i32 0, i32 0)]
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"struct_info::<Foo>::field_types" = private unnamed_addr constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
-@"struct_info::<Foo>::field_offsets" = private unnamed_addr constant [2 x i16] [i16 0, i16 4]
-@"type_info::<Foo>" = private unnamed_addr constant <{ [16 x i8], i8*, [16 x i8], i8**, [0 x i8], %"mun_codegen::ir::types::TypeInfo"**, [0 x i8], i16*, [8 x i8] }> <{ [16 x i8] c"\03\FC\D1_\DB\DB\0AJ1r,\F0m\CBQ\D0", i8* getelementptr inbounds ([4 x i8], [4 x i8]* @"type_info::<Foo>::name", i32 0, i32 0), [16 x i8] c"@\00\00\00\04\00\00\00\01\00\00\00\00\00\00\00", i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"struct_info::<Foo>::field_names", i32 0, i32 0), [0 x i8] zeroinitializer, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([2 x %"mun_codegen::ir::types::TypeInfo"*], [2 x %"mun_codegen::ir::types::TypeInfo"*]* @"struct_info::<Foo>::field_types", i32 0, i32 0), [0 x i8] zeroinitializer, i16* getelementptr inbounds ([2 x i16], [2 x i16]* @"struct_info::<Foo>::field_offsets", i32 0, i32 0), [8 x i8] c"\02\00\00\00\00\00\00\00" }>
-@"type_info::<*const TypeInfo>::name" = private unnamed_addr constant [16 x i8] c"*const TypeInfo\00"
-@"type_info::<*const TypeInfo>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"=\A1-\1F\C2\A7\88`d\90\F4\B5\BEE}x", i8* getelementptr inbounds ([16 x i8], [16 x i8]* @"type_info::<*const TypeInfo>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<*const *mut core::void>::name" = private unnamed_addr constant [23 x i8] c"*const *mut core::void\00"
-@"type_info::<*const *mut core::void>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\C5fO\BD\84\DF\06\BFd+\B1\9Abv\CE\00", i8* getelementptr inbounds ([23 x i8], [23 x i8]* @"type_info::<*const *mut core::void>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<*mut core::void>::name" = private unnamed_addr constant [16 x i8] c"*mut core::void\00"
-@"type_info::<*mut core::void>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\F0Y\22\FC\95\9E\7F\CE\08T\B1\A2\CD\A7\FAz", i8* getelementptr inbounds ([16 x i8], [16 x i8]* @"type_info::<*mut core::void>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [5 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<Foo>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*const TypeInfo>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*const *mut core::void>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*mut core::void>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 @allocatorHandle = unnamed_addr global i8* null
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__if_statement.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__if_statement.snap
index 8d5ab49e4..7b9c26167 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__if_statement.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__if_statement.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn foo(a:i32) -> i32 {\n        let b = if a > 3 {\n            let c = if a > 4 {\n                a+1\n            } else {\n                a+3\n            }\n            c\n        } else {\n            a-1\n        }\n        b\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i32 @foo(i32 %0) {
 body:
@@ -38,10 +34,5 @@ if_merge:                                         ; preds = %else, %then
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__issue_128.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__issue_128.snap
index dba8d0a05..866ece81c 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__issue_128.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__issue_128.snap
@@ -2,18 +2,15 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    // resources/script.mun\n    extern fn thing(n: i32);\n    extern fn print(n: i32) -> i32;\n\n    pub fn main() {\n        // 1st\n        print(1);\n        thing(5);\n\n        // 2nd\n        print(2);\n        thing(78);\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
 %DispatchTable = type { i32 (i32)*, void (i32)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = external global %DispatchTable
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [9 x i64*]
 
 define void @main() {
 body:
@@ -33,11 +30,7 @@ body:
 source_filename = "group_name"
 
 %DispatchTable = type { i32 (i32)*, void (i32)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = global %DispatchTable zeroinitializer
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [9 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__issue_225.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__issue_225.snap
index eedec7abf..f35b13092 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__issue_225.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__issue_225.snap
@@ -2,27 +2,24 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    struct Num {\n        value: i64,\n    }\n\n    pub fn foo(b: i64) {\n        Num { value: b }.value;\n    }\n\n    pub fn bar(b: i64) {\n        { let a = Num { value: b }; a}.value;\n    }\n        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
 %DispatchTable = type { i8** (i8*, i8*)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 %Num = type { i64 }
 
 @allocatorHandle = external global i8*
 @dispatchTable = external global %DispatchTable
-@global_type_table = external global [5 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [7 x i64*]
 
 define void @foo(i64 %0) {
 body:
   %init = insertvalue %Num undef, i64 %0, 0
   %new_ptr = load i8** (i8*, i8*)*, i8** (i8*, i8*)** getelementptr inbounds (%DispatchTable, %DispatchTable* @dispatchTable, i32 0, i32 0), align 8
-  %Num_ptr = load %"mun_codegen::ir::types::TypeInfo"*, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([5 x %"mun_codegen::ir::types::TypeInfo"*], [5 x %"mun_codegen::ir::types::TypeInfo"*]* @global_type_table, i64 0, i64 2), align 8
-  %type_info_ptr_to_i8_ptr = bitcast %"mun_codegen::ir::types::TypeInfo"* %Num_ptr to i8*
+  %Num_ptr = load i64*, i64** getelementptr inbounds ([7 x i64*], [7 x i64*]* @global_type_lookup_table, i64 0, i64 3), align 8
+  %type_info_ptr_to_i8_ptr = bitcast i64* %Num_ptr to i8*
   %allocator_handle = load i8*, i8** @allocatorHandle, align 8
   %new = call i8** %new_ptr(i8* %type_info_ptr_to_i8_ptr, i8* %allocator_handle)
   %Num_ptr_ptr = bitcast i8** %new to %Num**
@@ -37,8 +34,8 @@ define void @bar(i64 %0) {
 body:
   %init = insertvalue %Num undef, i64 %0, 0
   %new_ptr = load i8** (i8*, i8*)*, i8** (i8*, i8*)** getelementptr inbounds (%DispatchTable, %DispatchTable* @dispatchTable, i32 0, i32 0), align 8
-  %Num_ptr = load %"mun_codegen::ir::types::TypeInfo"*, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([5 x %"mun_codegen::ir::types::TypeInfo"*], [5 x %"mun_codegen::ir::types::TypeInfo"*]* @global_type_table, i64 0, i64 2), align 8
-  %type_info_ptr_to_i8_ptr = bitcast %"mun_codegen::ir::types::TypeInfo"* %Num_ptr to i8*
+  %Num_ptr = load i64*, i64** getelementptr inbounds ([7 x i64*], [7 x i64*]* @global_type_lookup_table, i64 0, i64 3), align 8
+  %type_info_ptr_to_i8_ptr = bitcast i64* %Num_ptr to i8*
   %allocator_handle = load i8*, i8** @allocatorHandle, align 8
   %new = call i8** %new_ptr(i8* %type_info_ptr_to_i8_ptr, i8* %allocator_handle)
   %Num_ptr_ptr = bitcast i8** %new to %Num**
@@ -54,24 +51,8 @@ body:
 source_filename = "group_name"
 
 %DispatchTable = type { i8** (i8*, i8*)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = global %DispatchTable zeroinitializer
-@"type_info::<*const TypeInfo>::name" = private unnamed_addr constant [16 x i8] c"*const TypeInfo\00"
-@"type_info::<*const TypeInfo>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"=\A1-\1F\C2\A7\88`d\90\F4\B5\BEE}x", i8* getelementptr inbounds ([16 x i8], [16 x i8]* @"type_info::<*const TypeInfo>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<core::i64>::name" = private unnamed_addr constant [10 x i8] c"core::i64\00"
-@"type_info::<core::i64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"G\13;t\97j8\18\D7M\83`\1D\C8\19%", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<Num>::name" = private unnamed_addr constant [4 x i8] c"Num\00"
-@"struct_info::<Num>::field_names.0" = private unnamed_addr constant [6 x i8] c"value\00"
-@"struct_info::<Num>::field_names" = private unnamed_addr constant [1 x i8*] [i8* getelementptr inbounds ([6 x i8], [6 x i8]* @"struct_info::<Num>::field_names.0", i32 0, i32 0)]
-@"struct_info::<Num>::field_types" = private unnamed_addr constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i64>"]
-@"struct_info::<Num>::field_offsets" = private unnamed_addr constant [1 x i16] zeroinitializer
-@"type_info::<Num>" = private unnamed_addr constant <{ [16 x i8], i8*, [16 x i8], i8**, [0 x i8], %"mun_codegen::ir::types::TypeInfo"**, [0 x i8], i16*, [8 x i8] }> <{ [16 x i8] c"\A92\E2p\B0\98\B2\C4\0C\A2\F5=x\904\00", i8* getelementptr inbounds ([4 x i8], [4 x i8]* @"type_info::<Num>::name", i32 0, i32 0), [16 x i8] c"@\00\00\00\08\00\00\00\01\00\00\00\00\00\00\00", i8** getelementptr inbounds ([1 x i8*], [1 x i8*]* @"struct_info::<Num>::field_names", i32 0, i32 0), [0 x i8] zeroinitializer, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([1 x %"mun_codegen::ir::types::TypeInfo"*], [1 x %"mun_codegen::ir::types::TypeInfo"*]* @"struct_info::<Num>::field_types", i32 0, i32 0), [0 x i8] zeroinitializer, i16* getelementptr inbounds ([1 x i16], [1 x i16]* @"struct_info::<Num>::field_offsets", i32 0, i32 0), [8 x i8] c"\01\00\00\00\00\00\00\00" }>
-@"type_info::<*const *mut core::void>::name" = private unnamed_addr constant [23 x i8] c"*const *mut core::void\00"
-@"type_info::<*const *mut core::void>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\C5fO\BD\84\DF\06\BFd+\B1\9Abv\CE\00", i8* getelementptr inbounds ([23 x i8], [23 x i8]* @"type_info::<*const *mut core::void>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<*mut core::void>::name" = private unnamed_addr constant [16 x i8] c"*mut core::void\00"
-@"type_info::<*mut core::void>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\F0Y\22\FC\95\9E\7F\CE\08T\B1\A2\CD\A7\FAz", i8* getelementptr inbounds ([16 x i8], [16 x i8]* @"type_info::<*mut core::void>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [5 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<*const TypeInfo>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i64>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<Num>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*const *mut core::void>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*mut core::void>"]
+@global_type_lookup_table = global [7 x i64*] zeroinitializer
 @allocatorHandle = unnamed_addr global i8* null
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__issue_228.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__issue_228.snap
index 00ff6974a..0596967ee 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__issue_228.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__issue_228.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub  fn fact(n: usize) -> usize {\n   \t    if n == 0 {return 1} else {n * (n-1)}\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i64 @fact(i64 %0) {
 body:
@@ -26,10 +22,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u64>::name" = private unnamed_addr constant [10 x i8] c"core::u64\00"
-@"type_info::<core::u64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\A6\E7g \D1\8B\1Aq`\1F\1E\07\BB5@q", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u64>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__issue_228_never_if.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__issue_228_never_if.snap
index ad2ff3b99..99a2bcf45 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__issue_228_never_if.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__issue_228_never_if.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub  fn fact(n: usize) -> usize {\n   \t    if n == 0 {return 1} else {return n * (n-1)}\n   \t    return 2;\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i64 @fact(i64 %0) {
 body:
@@ -31,10 +27,5 @@ else:                                             ; preds = %body
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u64>::name" = private unnamed_addr constant [10 x i8] c"core::u64\00"
-@"type_info::<core::u64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\A6\E7g \D1\8B\1Aq`\1F\1E\07\BB5@q", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u64>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__let_statement.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__let_statement.snap
index 4ca79e67c..eb30e10ee 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__let_statement.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__let_statement.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn main(a:i32) -> i32 {\n      let b = a+1\n      b\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i32 @main(i32 %0) {
 body:
@@ -23,10 +19,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__literal_types.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__literal_types.snap
index 1b2b06bce..7948dbb82 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__literal_types.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__literal_types.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn main(){\n        let a = 123;\n        let a = 123u8;\n        let a = 123u16;\n        let a = 123u32;\n        let a = 123u64;\n        let a = 123u128;\n        let a = 1_000_000_u32;\n        let a = 123i8;\n        let a = 123i16;\n        let a = 123i32;\n        let a = 123i64;\n        let a = 123123123123123123123123123123123i128;\n        let a = 1_000_000_i32;\n        let a = 1_000_123.0e-2;\n        let a = 1_000_123.0e-2f32;\n        let a = 1_000_123.0e-2f64;\n    }\n\n    pub fn add(a:u32) -> u32 {\n        a + 12u32\n    }"
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define void @main() {
 body:
@@ -63,10 +59,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u32>::name" = private unnamed_addr constant [10 x i8] c"core::u32\00"
-@"type_info::<core::u32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"daz5d\A6\BE\88\81=&Y\A1+\C6\1D", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u32>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__logic_op_bool.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__logic_op_bool.snap
index 680daba12..a163066a1 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__logic_op_bool.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__logic_op_bool.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn and(a: bool, b: bool) -> bool {\n        a && b\n    }\n    pub fn or(a: bool, b: bool) -> bool {\n        a || b\n    }    \n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i1 @and(i1 %0, i1 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__loop_break_expr.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__loop_break_expr.snap
index 09c09ccea..37facf4f9 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__loop_break_expr.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__loop_break_expr.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn foo(n:i32) -> i32 {\n        loop {\n            if n > 5 {\n                break n;\n            }\n            if n > 10 {\n                break 10;\n            }\n            n += 1;\n        }\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i32 @foo(i32 %0) {
 body:
@@ -42,10 +38,5 @@ if_merge6:                                        ; preds = %if_merge
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__multi_file.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__multi_file.snap
index 572be7210..931f126a1 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__multi_file.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__multi_file.snap
@@ -2,18 +2,15 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    //- /mod.mun\n    pub fn main() -> i32 {\n        foo::get_value()\n    }\n\n    //- /foo.mun\n    pub(super) fn get_value() -> i32 {\n        3\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
 %DispatchTable = type { i32 ()* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = external global %DispatchTable
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i32 @main() {
 body:
@@ -27,22 +24,15 @@ body:
 source_filename = "group_name"
 
 %DispatchTable = type { i32 ()* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = global %DispatchTable zeroinitializer
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
 ; == FILE IR (foo) =====================================
 ; ModuleID = 'foo'
 source_filename = "foo"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [1 x i64*]
 
 define i32 @get_value() {
 body:
@@ -53,10 +43,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [1 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_f32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_f32.snap
index 42fd0d40c..37dcc51f1 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_f32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_f32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn negate(a: f32) -> f32 { -a }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define float @negate(float %0) {
 body:
@@ -23,10 +19,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::f32>::name" = private unnamed_addr constant [10 x i8] c"core::f32\00"
-@"type_info::<core::f32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"P\19b7\A8k\F2\81P\FB\83\F5P\B0\82!", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::f32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f32>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_f64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_f64.snap
index ca40b7beb..ed2a91db8 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_f64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_f64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn negate(a: f64) -> f64 { -a }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define double @negate(double %0) {
 body:
@@ -23,10 +19,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::f64>::name" = private unnamed_addr constant [10 x i8] c"core::f64\00"
-@"type_info::<core::f64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"`\DBF\9C?YJ%G\AD4\9F\D5\92%A", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::f64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f64>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i128.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i128.snap
index c70c3bbe2..893063402 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i128.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i128.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn negate(a: i128) -> i128 { -a }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i128 @negate(i128 %0) {
 body:
@@ -23,10 +19,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i128>::name" = private unnamed_addr constant [11 x i8] c"core::i128\00"
-@"type_info::<core::i128>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\BDkp\09RRM\EBc\02\A0\DB47\A7\E3", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::i128>::name", i32 0, i32 0), [48 x i8] c"\80\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i128>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i16.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i16.snap
index f6f4de6df..afe405a8c 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i16.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i16.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn negate(a: i16) -> i16 { -a }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i16 @negate(i16 %0) {
 body:
@@ -23,10 +19,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i16>::name" = private unnamed_addr constant [10 x i8] c"core::i16\00"
-@"type_info::<core::i16>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\05\CD|\F8Bv\D8\B1\E8\8B\8C\D8\8D\B5\89\B0", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i16>::name", i32 0, i32 0), [48 x i8] c"\10\00\00\00\02\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i16>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i32.snap
index 04c17d8a1..d13224dd8 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn negate(a: i32) -> i32 { -a }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i32 @negate(i32 %0) {
 body:
@@ -23,10 +19,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i64.snap
index 40453abca..dfa701a1e 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn negate(a: i64) -> i64 { -a }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i64 @negate(i64 %0) {
 body:
@@ -23,10 +19,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i64>::name" = private unnamed_addr constant [10 x i8] c"core::i64\00"
-@"type_info::<core::i64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"G\13;t\97j8\18\D7M\83`\1D\C8\19%", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i64>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i8.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i8.snap
index 3a956b0cd..9396959d2 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i8.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__negate_op_i8.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn negate(a: i8) -> i8 { -a }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i8 @negate(i8 %0) {
 body:
@@ -23,10 +19,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i8>::name" = private unnamed_addr constant [9 x i8] c"core::i8\00"
-@"type_info::<core::i8>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\EF\C4\B1Z\E7\12\B1\91q\F1\0B\80U\FC\A6\0F", i8* getelementptr inbounds ([9 x i8], [9 x i8]* @"type_info::<core::i8>::name", i32 0, i32 0), [48 x i8] c"\08\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i8>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_private_extern_fn.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_private_extern_fn.snap
index 47784666e..e8d0a873a 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_private_extern_fn.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_private_extern_fn.snap
@@ -2,18 +2,15 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    extern fn extern_fn() -> f32;\n\n    fn private_fn() -> f32 {\n        extern_fn()\n    }\n\n    pub fn main() -> f32 {\n        private_fn()\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
 %DispatchTable = type { float ()* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = external global %DispatchTable
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [3 x i64*]
 
 define float @private_fn() {
 body:
@@ -33,11 +30,7 @@ body:
 source_filename = "group_name"
 
 %DispatchTable = type { float ()* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = global %DispatchTable zeroinitializer
-@"type_info::<core::f32>::name" = private unnamed_addr constant [10 x i8] c"core::f32\00"
-@"type_info::<core::f32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"P\19b7\A8k\F2\81P\FB\83\F5P\B0\82!", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::f32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f32>"]
+@global_type_lookup_table = global [3 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_private_fn.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_private_fn.snap
index c0fd6779c..78b46cbdd 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_private_fn.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_private_fn.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    fn nested_private_fn() -> i32 {\n        1\n    }\n\n    fn private_fn() -> i32 {\n        nested_private_fn()\n    }\n\n    pub fn main() -> i32 {\n        private_fn()\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [1 x i64*]
 
 define i32 @nested_private_fn() {
 body:
@@ -34,10 +30,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [1 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_private_recursive_fn.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_private_recursive_fn.snap
index 69f833df3..5baa1613e 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_private_recursive_fn.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_private_recursive_fn.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    fn private_fn() -> f32 {\n        private_fn()\n    }\n\n    pub fn main() -> f32 {\n        private_fn()\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [1 x i64*]
 
 define float @private_fn() {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::f32>::name" = private unnamed_addr constant [10 x i8] c"core::f32\00"
-@"type_info::<core::f32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"P\19b7\A8k\F2\81P\FB\83\F5P\B0\82!", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::f32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f32>"]
+@global_type_lookup_table = global [1 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_private_recursive_fn_with_args.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_private_recursive_fn_with_args.snap
index d3f3c7b94..e44bca343 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_private_recursive_fn_with_args.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_private_recursive_fn_with_args.snap
@@ -2,18 +2,15 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    extern fn other() -> i32;\n\n    fn private_fn(a: i32) -> f32 {\n        private_fn(a)\n    }\n\n    pub fn main() -> f32 {\n        private_fn(other())\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
 %DispatchTable = type { i32 ()* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = external global %DispatchTable
-@global_type_table = external global [2 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [3 x i64*]
 
 define float @private_fn(i32 %0) {
 body:
@@ -34,13 +31,7 @@ body:
 source_filename = "group_name"
 
 %DispatchTable = type { i32 ()* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = global %DispatchTable zeroinitializer
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<core::f32>::name" = private unnamed_addr constant [10 x i8] c"core::f32\00"
-@"type_info::<core::f32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"P\19b7\A8k\F2\81P\FB\83\F5P\B0\82!", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::f32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f32>"]
+@global_type_lookup_table = global [3 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_structs.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_structs.snap
index ba3c428e4..aeab23657 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_structs.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__nested_structs.snap
@@ -2,15 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub struct(gc) GcStruct(f32, f32);\n    pub struct(value) ValueStruct(f32, f32);\n\n    pub struct(gc) GcWrapper(GcStruct, ValueStruct)\n    pub struct(value) ValueWrapper(GcStruct, ValueStruct);\n\n    pub fn new_gc_struct(a: f32, b: f32) -> GcStruct {\n        GcStruct(a, b)\n    }\n\n    pub fn new_value_struct(a: f32, b: f32) -> ValueStruct {\n        ValueStruct(a, b)\n    }\n\n    pub fn new_gc_wrapper(a: GcStruct, b: ValueStruct) -> GcWrapper {\n        GcWrapper(a, b)\n    }\n\n    pub fn new_value_wrapper(a: GcStruct, b: ValueStruct) -> ValueWrapper {\n        ValueWrapper(a, b)\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
 %DispatchTable = type { i8** (i8*, i8*)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 %GcStruct = type { float, float }
 %ValueStruct = type { float, float }
 %GcWrapper = type { %GcStruct**, %ValueStruct }
@@ -18,15 +15,15 @@ source_filename = "mod"
 
 @allocatorHandle = external global i8*
 @dispatchTable = external global %DispatchTable
-@global_type_table = external global [8 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [59 x i64*]
 
 define %GcStruct** @new_gc_struct(float %0, float %1) {
 body:
   %init = insertvalue %GcStruct undef, float %0, 0
   %init3 = insertvalue %GcStruct %init, float %1, 1
   %new_ptr = load i8** (i8*, i8*)*, i8** (i8*, i8*)** getelementptr inbounds (%DispatchTable, %DispatchTable* @dispatchTable, i32 0, i32 0), align 8
-  %GcStruct_ptr = load %"mun_codegen::ir::types::TypeInfo"*, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([8 x %"mun_codegen::ir::types::TypeInfo"*], [8 x %"mun_codegen::ir::types::TypeInfo"*]* @global_type_table, i64 0, i64 5), align 8
-  %type_info_ptr_to_i8_ptr = bitcast %"mun_codegen::ir::types::TypeInfo"* %GcStruct_ptr to i8*
+  %GcStruct_ptr = load i64*, i64** getelementptr inbounds ([59 x i64*], [59 x i64*]* @global_type_lookup_table, i64 0, i64 53), align 8
+  %type_info_ptr_to_i8_ptr = bitcast i64* %GcStruct_ptr to i8*
   %allocator_handle = load i8*, i8** @allocatorHandle, align 8
   %new = call i8** %new_ptr(i8* %type_info_ptr_to_i8_ptr, i8* %allocator_handle)
   %GcStruct_ptr_ptr = bitcast i8** %new to %GcStruct**
@@ -46,8 +43,8 @@ define %ValueStruct** @new_value_struct_wrapper(float %0, float %1) {
 body:
   %new_value_struct = call %ValueStruct @new_value_struct(float %0, float %1)
   %new_ptr = load i8** (i8*, i8*)*, i8** (i8*, i8*)** getelementptr inbounds (%DispatchTable, %DispatchTable* @dispatchTable, i32 0, i32 0), align 8
-  %ValueStruct_ptr = load %"mun_codegen::ir::types::TypeInfo"*, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([8 x %"mun_codegen::ir::types::TypeInfo"*], [8 x %"mun_codegen::ir::types::TypeInfo"*]* @global_type_table, i64 0, i64 4), align 8
-  %type_info_ptr_to_i8_ptr = bitcast %"mun_codegen::ir::types::TypeInfo"* %ValueStruct_ptr to i8*
+  %ValueStruct_ptr = load i64*, i64** getelementptr inbounds ([59 x i64*], [59 x i64*]* @global_type_lookup_table, i64 0, i64 56), align 8
+  %type_info_ptr_to_i8_ptr = bitcast i64* %ValueStruct_ptr to i8*
   %allocator_handle = load i8*, i8** @allocatorHandle, align 8
   %new = call i8** %new_ptr(i8* %type_info_ptr_to_i8_ptr, i8* %allocator_handle)
   %ValueStruct_ptr_ptr = bitcast i8** %new to %ValueStruct**
@@ -63,8 +60,8 @@ body:
   %init = insertvalue %GcWrapper undef, %GcStruct** %0, 0
   %init3 = insertvalue %GcWrapper %init, %ValueStruct %1, 1
   %new_ptr = load i8** (i8*, i8*)*, i8** (i8*, i8*)** getelementptr inbounds (%DispatchTable, %DispatchTable* @dispatchTable, i32 0, i32 0), align 8
-  %GcWrapper_ptr = load %"mun_codegen::ir::types::TypeInfo"*, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([8 x %"mun_codegen::ir::types::TypeInfo"*], [8 x %"mun_codegen::ir::types::TypeInfo"*]* @global_type_table, i64 0, i64 0), align 8
-  %type_info_ptr_to_i8_ptr = bitcast %"mun_codegen::ir::types::TypeInfo"* %GcWrapper_ptr to i8*
+  %GcWrapper_ptr = load i64*, i64** getelementptr inbounds ([59 x i64*], [59 x i64*]* @global_type_lookup_table, i64 0, i64 39), align 8
+  %type_info_ptr_to_i8_ptr = bitcast i64* %GcWrapper_ptr to i8*
   %allocator_handle = load i8*, i8** @allocatorHandle, align 8
   %new = call i8** %new_ptr(i8* %type_info_ptr_to_i8_ptr, i8* %allocator_handle)
   %GcWrapper_ptr_ptr = bitcast i8** %new to %GcWrapper**
@@ -96,8 +93,8 @@ body:
   %deref = load %ValueStruct, %ValueStruct* %mem_ptr, align 4
   %new_value_wrapper = call %ValueWrapper @new_value_wrapper(%GcStruct** %0, %ValueStruct %deref)
   %new_ptr = load i8** (i8*, i8*)*, i8** (i8*, i8*)** getelementptr inbounds (%DispatchTable, %DispatchTable* @dispatchTable, i32 0, i32 0), align 8
-  %ValueWrapper_ptr = load %"mun_codegen::ir::types::TypeInfo"*, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([8 x %"mun_codegen::ir::types::TypeInfo"*], [8 x %"mun_codegen::ir::types::TypeInfo"*]* @global_type_table, i64 0, i64 2), align 8
-  %type_info_ptr_to_i8_ptr = bitcast %"mun_codegen::ir::types::TypeInfo"* %ValueWrapper_ptr to i8*
+  %ValueWrapper_ptr = load i64*, i64** getelementptr inbounds ([59 x i64*], [59 x i64*]* @global_type_lookup_table, i64 0, i64 52), align 8
+  %type_info_ptr_to_i8_ptr = bitcast i64* %ValueWrapper_ptr to i8*
   %allocator_handle = load i8*, i8** @allocatorHandle, align 8
   %new = call i8** %new_ptr(i8* %type_info_ptr_to_i8_ptr, i8* %allocator_handle)
   %ValueWrapper_ptr_ptr = bitcast i8** %new to %ValueWrapper**
@@ -111,46 +108,8 @@ body:
 source_filename = "group_name"
 
 %DispatchTable = type { i8** (i8*, i8*)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = global %DispatchTable zeroinitializer
-@"type_info::<GcWrapper>::name" = private unnamed_addr constant [10 x i8] c"GcWrapper\00"
-@"struct_info::<GcWrapper>::field_names.0" = private unnamed_addr constant [2 x i8] c"0\00"
-@"struct_info::<GcWrapper>::field_names.1" = private unnamed_addr constant [2 x i8] c"1\00"
-@"struct_info::<GcWrapper>::field_names" = private unnamed_addr constant [2 x i8*] [i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<GcWrapper>::field_names.0", i32 0, i32 0), i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<GcWrapper>::field_names.1", i32 0, i32 0)]
-@"type_info::<GcStruct>::name" = private unnamed_addr constant [9 x i8] c"GcStruct\00"
-@"struct_info::<GcStruct>::field_names.0" = private unnamed_addr constant [2 x i8] c"0\00"
-@"struct_info::<GcStruct>::field_names.1" = private unnamed_addr constant [2 x i8] c"1\00"
-@"struct_info::<GcStruct>::field_names" = private unnamed_addr constant [2 x i8*] [i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<GcStruct>::field_names.0", i32 0, i32 0), i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<GcStruct>::field_names.1", i32 0, i32 0)]
-@"type_info::<core::f32>::name" = private unnamed_addr constant [10 x i8] c"core::f32\00"
-@"type_info::<core::f32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"P\19b7\A8k\F2\81P\FB\83\F5P\B0\82!", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::f32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"struct_info::<GcStruct>::field_types" = private unnamed_addr constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f32>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f32>"]
-@"struct_info::<GcStruct>::field_offsets" = private unnamed_addr constant [2 x i16] [i16 0, i16 4]
-@"type_info::<GcStruct>" = private unnamed_addr constant <{ [16 x i8], i8*, [16 x i8], i8**, [0 x i8], %"mun_codegen::ir::types::TypeInfo"**, [0 x i8], i16*, [8 x i8] }> <{ [16 x i8] c"\B9)lg\01\95k@E\B4(\CB\CAGX\E1", i8* getelementptr inbounds ([9 x i8], [9 x i8]* @"type_info::<GcStruct>::name", i32 0, i32 0), [16 x i8] c"@\00\00\00\04\00\00\00\01\00\00\00\00\00\00\00", i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"struct_info::<GcStruct>::field_names", i32 0, i32 0), [0 x i8] zeroinitializer, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([2 x %"mun_codegen::ir::types::TypeInfo"*], [2 x %"mun_codegen::ir::types::TypeInfo"*]* @"struct_info::<GcStruct>::field_types", i32 0, i32 0), [0 x i8] zeroinitializer, i16* getelementptr inbounds ([2 x i16], [2 x i16]* @"struct_info::<GcStruct>::field_offsets", i32 0, i32 0), [8 x i8] c"\02\00\00\00\00\00\00\00" }>
-@"type_info::<ValueStruct>::name" = private unnamed_addr constant [12 x i8] c"ValueStruct\00"
-@"struct_info::<ValueStruct>::field_names.0" = private unnamed_addr constant [2 x i8] c"0\00"
-@"struct_info::<ValueStruct>::field_names.1" = private unnamed_addr constant [2 x i8] c"1\00"
-@"struct_info::<ValueStruct>::field_names" = private unnamed_addr constant [2 x i8*] [i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<ValueStruct>::field_names.0", i32 0, i32 0), i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<ValueStruct>::field_names.1", i32 0, i32 0)]
-@"struct_info::<ValueStruct>::field_types" = private unnamed_addr constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f32>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f32>"]
-@"struct_info::<ValueStruct>::field_offsets" = private unnamed_addr constant [2 x i16] [i16 0, i16 4]
-@"type_info::<ValueStruct>" = private unnamed_addr constant <{ [16 x i8], i8*, [16 x i8], i8**, [0 x i8], %"mun_codegen::ir::types::TypeInfo"**, [0 x i8], i16*, [8 x i8] }> <{ [16 x i8] c"U0{\87\\\04Q/\95!$\A2\F1\A9\F9W", i8* getelementptr inbounds ([12 x i8], [12 x i8]* @"type_info::<ValueStruct>::name", i32 0, i32 0), [16 x i8] c"@\00\00\00\04\00\00\00\01\00\00\00\00\00\00\00", i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"struct_info::<ValueStruct>::field_names", i32 0, i32 0), [0 x i8] zeroinitializer, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([2 x %"mun_codegen::ir::types::TypeInfo"*], [2 x %"mun_codegen::ir::types::TypeInfo"*]* @"struct_info::<ValueStruct>::field_types", i32 0, i32 0), [0 x i8] zeroinitializer, i16* getelementptr inbounds ([2 x i16], [2 x i16]* @"struct_info::<ValueStruct>::field_offsets", i32 0, i32 0), [8 x i8] c"\02\00\01\00\00\00\00\00" }>
-@"struct_info::<GcWrapper>::field_types" = private unnamed_addr constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<GcStruct>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<ValueStruct>"]
-@"struct_info::<GcWrapper>::field_offsets" = private unnamed_addr constant [2 x i16] [i16 0, i16 8]
-@"type_info::<GcWrapper>" = private unnamed_addr constant <{ [16 x i8], i8*, [16 x i8], i8**, [0 x i8], %"mun_codegen::ir::types::TypeInfo"**, [0 x i8], i16*, [8 x i8] }> <{ [16 x i8] c"!\14\93\A7H1?90\B7\EA\DB0\82\A0\C7", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<GcWrapper>::name", i32 0, i32 0), [16 x i8] c"\80\00\00\00\08\00\00\00\01\00\00\00\00\00\00\00", i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"struct_info::<GcWrapper>::field_names", i32 0, i32 0), [0 x i8] zeroinitializer, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([2 x %"mun_codegen::ir::types::TypeInfo"*], [2 x %"mun_codegen::ir::types::TypeInfo"*]* @"struct_info::<GcWrapper>::field_types", i32 0, i32 0), [0 x i8] zeroinitializer, i16* getelementptr inbounds ([2 x i16], [2 x i16]* @"struct_info::<GcWrapper>::field_offsets", i32 0, i32 0), [8 x i8] c"\02\00\00\00\00\00\00\00" }>
-@"type_info::<*const TypeInfo>::name" = private unnamed_addr constant [16 x i8] c"*const TypeInfo\00"
-@"type_info::<*const TypeInfo>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"=\A1-\1F\C2\A7\88`d\90\F4\B5\BEE}x", i8* getelementptr inbounds ([16 x i8], [16 x i8]* @"type_info::<*const TypeInfo>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<ValueWrapper>::name" = private unnamed_addr constant [13 x i8] c"ValueWrapper\00"
-@"struct_info::<ValueWrapper>::field_names.0" = private unnamed_addr constant [2 x i8] c"0\00"
-@"struct_info::<ValueWrapper>::field_names.1" = private unnamed_addr constant [2 x i8] c"1\00"
-@"struct_info::<ValueWrapper>::field_names" = private unnamed_addr constant [2 x i8*] [i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<ValueWrapper>::field_names.0", i32 0, i32 0), i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<ValueWrapper>::field_names.1", i32 0, i32 0)]
-@"struct_info::<ValueWrapper>::field_types" = private unnamed_addr constant [2 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<GcStruct>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<ValueStruct>"]
-@"struct_info::<ValueWrapper>::field_offsets" = private unnamed_addr constant [2 x i16] [i16 0, i16 8]
-@"type_info::<ValueWrapper>" = private unnamed_addr constant <{ [16 x i8], i8*, [16 x i8], i8**, [0 x i8], %"mun_codegen::ir::types::TypeInfo"**, [0 x i8], i16*, [8 x i8] }> <{ [16 x i8] c"@j\D8\CD~-\12\87|A\E8\DBp\EC}\AA", i8* getelementptr inbounds ([13 x i8], [13 x i8]* @"type_info::<ValueWrapper>::name", i32 0, i32 0), [16 x i8] c"\80\00\00\00\08\00\00\00\01\00\00\00\00\00\00\00", i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"struct_info::<ValueWrapper>::field_names", i32 0, i32 0), [0 x i8] zeroinitializer, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([2 x %"mun_codegen::ir::types::TypeInfo"*], [2 x %"mun_codegen::ir::types::TypeInfo"*]* @"struct_info::<ValueWrapper>::field_types", i32 0, i32 0), [0 x i8] zeroinitializer, i16* getelementptr inbounds ([2 x i16], [2 x i16]* @"struct_info::<ValueWrapper>::field_offsets", i32 0, i32 0), [8 x i8] c"\02\00\01\00\00\00\00\00" }>
-@"type_info::<*const *mut core::void>::name" = private unnamed_addr constant [23 x i8] c"*const *mut core::void\00"
-@"type_info::<*const *mut core::void>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\C5fO\BD\84\DF\06\BFd+\B1\9Abv\CE\00", i8* getelementptr inbounds ([23 x i8], [23 x i8]* @"type_info::<*const *mut core::void>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<*mut core::void>::name" = private unnamed_addr constant [16 x i8] c"*mut core::void\00"
-@"type_info::<*mut core::void>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\F0Y\22\FC\95\9E\7F\CE\08T\B1\A2\CD\A7\FAz", i8* getelementptr inbounds ([16 x i8], [16 x i8]* @"type_info::<*mut core::void>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [8 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<GcWrapper>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*const TypeInfo>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<ValueWrapper>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f32>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<ValueStruct>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<GcStruct>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*const *mut core::void>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*mut core::void>"]
+@global_type_lookup_table = global [59 x i64*] zeroinitializer
 @allocatorHandle = unnamed_addr global i8* null
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__never_conditional_return_expr.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__never_conditional_return_expr.snap
index 012196e2d..199a54b26 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__never_conditional_return_expr.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__never_conditional_return_expr.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn main(a:i32) -> i32 {\n        if a > 4 {\n            return a;\n        } else {\n            return a - 1;\n        }\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i32 @main(i32 %0) {
 body:
@@ -30,10 +26,5 @@ else:                                             ; preds = %body
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__return_expr.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__return_expr.snap
index 00f3cca81..dc80464f2 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__return_expr.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__return_expr.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn main() -> i32 {\n        return 5;\n        let a = 3; // Nothing regarding this statement should be in the IR\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [1 x i64*]
 
 define i32 @main() {
 body:
@@ -22,10 +18,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [1 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__return_type.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__return_type.snap
index 2f81689f4..2802d14d3 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__return_type.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__return_type.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn main() -> i32 {\n      0\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [1 x i64*]
 
 define i32 @main() {
 body:
@@ -22,10 +18,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [1 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__shadowing.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__shadowing.snap
index 04a353aa8..097263068 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__shadowing.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__shadowing.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn foo(a:i32) -> i32 {\n        let a = a+1;\n        {\n            let a = a+2;\n        }\n        a+3\n    }\n\n    pub fn bar(a:i32) -> i32 {\n        let a = a+1;\n        let a = {\n            let a = a+2;\n            a\n        }\n        a+3\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [4 x i64*]
 
 define i32 @foo(i32 %0) {
 body:
@@ -32,10 +28,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [4 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i128.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i128.snap
index 0dac6f8a9..52ad8ac2d 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i128.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i128.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn leftshift(a: i128, b: i128) -> i128 { a << b }\n    pub fn rightshift(a: i128, b: i128) -> i128 { a >> b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i128 @leftshift(i128 %0, i128 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i128>::name" = private unnamed_addr constant [11 x i8] c"core::i128\00"
-@"type_info::<core::i128>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\BDkp\09RRM\EBc\02\A0\DB47\A7\E3", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::i128>::name", i32 0, i32 0), [48 x i8] c"\80\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i128>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i16.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i16.snap
index 867c17455..6299c6416 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i16.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i16.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn leftshift(a: i16, b: i16) -> i16 { a << b }\n    pub fn rightshift(a: i16, b: i16) -> i16 { a >> b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i16 @leftshift(i16 %0, i16 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i16>::name" = private unnamed_addr constant [10 x i8] c"core::i16\00"
-@"type_info::<core::i16>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\05\CD|\F8Bv\D8\B1\E8\8B\8C\D8\8D\B5\89\B0", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i16>::name", i32 0, i32 0), [48 x i8] c"\10\00\00\00\02\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i16>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i32.snap
index 370500ee9..1b3b99937 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn leftshift(a: i32, b: i32) -> i32 { a << b }\n    pub fn rightshift(a: i32, b: i32) -> i32 { a >> b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i32 @leftshift(i32 %0, i32 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i64.snap
index 5808fd808..d6202b642 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn leftshift(a: i64, b: i64) -> i64 { a << b }\n    pub fn rightshift(a: i64, b: i64) -> i64 { a >> b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i64 @leftshift(i64 %0, i64 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i64>::name" = private unnamed_addr constant [10 x i8] c"core::i64\00"
-@"type_info::<core::i64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"G\13;t\97j8\18\D7M\83`\1D\C8\19%", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i64>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i8.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i8.snap
index ff59b90cc..ec956e6b7 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i8.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_i8.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn leftshift(a: i8, b: i8) -> i8 { a << b }\n    pub fn rightshift(a: i8, b: i8) -> i8 { a >> b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i8 @leftshift(i8 %0, i8 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i8>::name" = private unnamed_addr constant [9 x i8] c"core::i8\00"
-@"type_info::<core::i8>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\EF\C4\B1Z\E7\12\B1\91q\F1\0B\80U\FC\A6\0F", i8* getelementptr inbounds ([9 x i8], [9 x i8]* @"type_info::<core::i8>::name", i32 0, i32 0), [48 x i8] c"\08\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i8>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u128.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u128.snap
index cf4cc8f06..89541c1b9 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u128.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u128.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn leftshift(a: u128, b: u128) -> u128 { a << b }\n    pub fn rightshift(a: u128, b: u128) -> u128 { a >> b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i128 @leftshift(i128 %0, i128 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u128>::name" = private unnamed_addr constant [11 x i8] c"core::u128\00"
-@"type_info::<core::u128>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\E67\1BU\E9k\95\93d\14}\1C\96S\95\F0", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::u128>::name", i32 0, i32 0), [48 x i8] c"\80\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u128>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u16.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u16.snap
index 998b99da7..5b432b714 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u16.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u16.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn leftshift(a: u16, b: u16) -> u16 { a << b }\n    pub fn rightshift(a: u16, b: u16) -> u16 { a >> b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i16 @leftshift(i16 %0, i16 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u16>::name" = private unnamed_addr constant [10 x i8] c"core::u16\00"
-@"type_info::<core::u16>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"0\01\BC\BBK\E0\F2\7F&l\01\CD|q\F2\B3", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u16>::name", i32 0, i32 0), [48 x i8] c"\10\00\00\00\02\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u16>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u32.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u32.snap
index 0d22d994d..c484a2158 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u32.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u32.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn leftshift(a: u32, b: u32) -> u32 { a << b }\n    pub fn rightshift(a: u32, b: u32) -> u32 { a >> b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i32 @leftshift(i32 %0, i32 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u32>::name" = private unnamed_addr constant [10 x i8] c"core::u32\00"
-@"type_info::<core::u32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"daz5d\A6\BE\88\81=&Y\A1+\C6\1D", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u32>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u64.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u64.snap
index fcefb87e5..84ccac2da 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u64.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u64.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn leftshift(a: u64, b: u64) -> u64 { a << b }\n    pub fn rightshift(a: u64, b: u64) -> u64 { a >> b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i64 @leftshift(i64 %0, i64 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u64>::name" = private unnamed_addr constant [10 x i8] c"core::u64\00"
-@"type_info::<core::u64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\A6\E7g \D1\8B\1Aq`\1F\1E\07\BB5@q", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::u64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u64>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u8.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u8.snap
index c5502217a..05e68b245 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u8.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__shift_op_u8.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn leftshift(a: u8, b: u8) -> u8 { a << b }\n    pub fn rightshift(a: u8, b: u8) -> u8 { a >> b }\n                        "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [6 x i64*]
 
 define i8 @leftshift(i8 %0, i8 %1) {
 body:
@@ -29,10 +25,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::u8>::name" = private unnamed_addr constant [9 x i8] c"core::u8\00"
-@"type_info::<core::u8>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\A0y\A7S\B6(n\F7f&H\E1\F9\AD\04>", i8* getelementptr inbounds ([9 x i8], [9 x i8]* @"type_info::<core::u8>::name", i32 0, i32 0), [48 x i8] c"\08\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::u8>"]
+@global_type_lookup_table = global [6 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__struct_test.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__struct_test.snap
index 7f8ba31aa..fe95ad4c1 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__struct_test.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__struct_test.snap
@@ -2,22 +2,19 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    struct(value) Bar(f64, i32, bool, Foo);\n    struct(value) Foo { a: i32 };\n    struct(value) Baz;\n    pub fn foo() {\n        let a: Foo = Foo { a: 5 };\n        let b: Bar = Bar(1.23, a.a, true, a);\n        let c: Baz = Baz;\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
 %DispatchTable = type { i8** (i8*, i8*)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 %Baz = type {}
 %Bar = type { double, i32, i1, %Foo }
 %Foo = type { i32 }
 
 @allocatorHandle = external global i8*
 @dispatchTable = external global %DispatchTable
-@global_type_table = external global [9 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [12 x i64*]
 
 define void @foo() {
 body:
@@ -41,39 +38,8 @@ body:
 source_filename = "group_name"
 
 %DispatchTable = type { i8** (i8*, i8*)* }
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
 
 @dispatchTable = global %DispatchTable zeroinitializer
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<Foo>::name" = private unnamed_addr constant [4 x i8] c"Foo\00"
-@"struct_info::<Foo>::field_names.0" = private unnamed_addr constant [2 x i8] c"a\00"
-@"struct_info::<Foo>::field_names" = private unnamed_addr constant [1 x i8*] [i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<Foo>::field_names.0", i32 0, i32 0)]
-@"struct_info::<Foo>::field_types" = private unnamed_addr constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
-@"struct_info::<Foo>::field_offsets" = private unnamed_addr constant [1 x i16] zeroinitializer
-@"type_info::<Foo>" = private unnamed_addr constant <{ [16 x i8], i8*, [16 x i8], i8**, [0 x i8], %"mun_codegen::ir::types::TypeInfo"**, [0 x i8], i16*, [8 x i8] }> <{ [16 x i8] c"8\AD\C9\D9Y\D2\AA<ME}\CD.\A5\A0\DB", i8* getelementptr inbounds ([4 x i8], [4 x i8]* @"type_info::<Foo>::name", i32 0, i32 0), [16 x i8] c" \00\00\00\04\00\00\00\01\00\00\00\00\00\00\00", i8** getelementptr inbounds ([1 x i8*], [1 x i8*]* @"struct_info::<Foo>::field_names", i32 0, i32 0), [0 x i8] zeroinitializer, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([1 x %"mun_codegen::ir::types::TypeInfo"*], [1 x %"mun_codegen::ir::types::TypeInfo"*]* @"struct_info::<Foo>::field_types", i32 0, i32 0), [0 x i8] zeroinitializer, i16* getelementptr inbounds ([1 x i16], [1 x i16]* @"struct_info::<Foo>::field_offsets", i32 0, i32 0), [8 x i8] c"\01\00\01\00\00\00\00\00" }>
-@"type_info::<*const TypeInfo>::name" = private unnamed_addr constant [16 x i8] c"*const TypeInfo\00"
-@"type_info::<*const TypeInfo>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"=\A1-\1F\C2\A7\88`d\90\F4\B5\BEE}x", i8* getelementptr inbounds ([16 x i8], [16 x i8]* @"type_info::<*const TypeInfo>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<Baz>::name" = private unnamed_addr constant [4 x i8] c"Baz\00"
-@"type_info::<Baz>" = private unnamed_addr constant <{ [16 x i8], i8*, [16 x i8], i8**, [0 x i8], %"mun_codegen::ir::types::TypeInfo"**, [0 x i8], i16*, [8 x i8] }> <{ [16 x i8] c">\1A\BC\E5\C9\D3n\D8\8C?\86\22\FA\0DtV", i8* getelementptr inbounds ([4 x i8], [4 x i8]* @"type_info::<Baz>::name", i32 0, i32 0), [16 x i8] c"\00\00\00\00\01\00\00\00\01\00\00\00\00\00\00\00", i8** null, [0 x i8] zeroinitializer, %"mun_codegen::ir::types::TypeInfo"** null, [0 x i8] zeroinitializer, i16* null, [8 x i8] c"\00\00\01\00\00\00\00\00" }>
-@"type_info::<core::f64>::name" = private unnamed_addr constant [10 x i8] c"core::f64\00"
-@"type_info::<core::f64>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"`\DBF\9C?YJ%G\AD4\9F\D5\92%A", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::f64>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<*const *mut core::void>::name" = private unnamed_addr constant [23 x i8] c"*const *mut core::void\00"
-@"type_info::<*const *mut core::void>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\C5fO\BD\84\DF\06\BFd+\B1\9Abv\CE\00", i8* getelementptr inbounds ([23 x i8], [23 x i8]* @"type_info::<*const *mut core::void>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@"type_info::<Bar>::name" = private unnamed_addr constant [4 x i8] c"Bar\00"
-@"struct_info::<Bar>::field_names.0" = private unnamed_addr constant [2 x i8] c"0\00"
-@"struct_info::<Bar>::field_names.1" = private unnamed_addr constant [2 x i8] c"1\00"
-@"struct_info::<Bar>::field_names.2" = private unnamed_addr constant [2 x i8] c"2\00"
-@"struct_info::<Bar>::field_names.3" = private unnamed_addr constant [2 x i8] c"3\00"
-@"struct_info::<Bar>::field_names" = private unnamed_addr constant [4 x i8*] [i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<Bar>::field_names.0", i32 0, i32 0), i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<Bar>::field_names.1", i32 0, i32 0), i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<Bar>::field_names.2", i32 0, i32 0), i8* getelementptr inbounds ([2 x i8], [2 x i8]* @"struct_info::<Bar>::field_names.3", i32 0, i32 0)]
-@"struct_info::<Bar>::field_types" = private unnamed_addr constant [4 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f64>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<Foo>"]
-@"struct_info::<Bar>::field_offsets" = private unnamed_addr constant [4 x i16] [i16 0, i16 8, i16 12, i16 16]
-@"type_info::<Bar>" = private unnamed_addr constant <{ [16 x i8], i8*, [16 x i8], i8**, [0 x i8], %"mun_codegen::ir::types::TypeInfo"**, [0 x i8], i16*, [8 x i8] }> <{ [16 x i8] c"\D6\CA\E2\C3Ht\09\EA\AEh\E50L\F7\EE\B5", i8* getelementptr inbounds ([4 x i8], [4 x i8]* @"type_info::<Bar>::name", i32 0, i32 0), [16 x i8] c"\C0\00\00\00\08\00\00\00\01\00\00\00\00\00\00\00", i8** getelementptr inbounds ([4 x i8*], [4 x i8*]* @"struct_info::<Bar>::field_names", i32 0, i32 0), [0 x i8] zeroinitializer, %"mun_codegen::ir::types::TypeInfo"** getelementptr inbounds ([4 x %"mun_codegen::ir::types::TypeInfo"*], [4 x %"mun_codegen::ir::types::TypeInfo"*]* @"struct_info::<Bar>::field_types", i32 0, i32 0), [0 x i8] zeroinitializer, i16* getelementptr inbounds ([4 x i16], [4 x i16]* @"struct_info::<Bar>::field_offsets", i32 0, i32 0), [8 x i8] c"\04\00\01\00\00\00\00\00" }>
-@"type_info::<*mut core::void>::name" = private unnamed_addr constant [16 x i8] c"*mut core::void\00"
-@"type_info::<*mut core::void>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\F0Y\22\FC\95\9E\7F\CE\08T\B1\A2\CD\A7\FAz", i8* getelementptr inbounds ([16 x i8], [16 x i8]* @"type_info::<*mut core::void>::name", i32 0, i32 0), [48 x i8] c"@\00\00\00\08\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [9 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<Foo>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*const TypeInfo>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<Baz>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::f64>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*const *mut core::void>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<Bar>", %"mun_codegen::ir::types::TypeInfo"* @"type_info::<*mut core::void>"]
+@global_type_lookup_table = global [12 x i64*] zeroinitializer
 @allocatorHandle = unnamed_addr global i8* null
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__true_is_true.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__true_is_true.snap
index 5a0180a31..19aadf54e 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__true_is_true.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__true_is_true.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn test_true() -> bool {\n        true\n    }\n\n    pub fn test_false() -> bool {\n        false\n    }"
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i1 @test_true() {
 body:
@@ -27,10 +23,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::bool>::name" = private unnamed_addr constant [11 x i8] c"core::bool\00"
-@"type_info::<core::bool>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"x\82\81m t7\03\CB\F8k\81-;\C9\84", i8* getelementptr inbounds ([11 x i8], [11 x i8]* @"type_info::<core::bool>::name", i32 0, i32 0), [48 x i8] c"\01\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::bool>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__update_operators.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__update_operators.snap
index d19dd8cf6..2ea1a8f1e 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__update_operators.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__update_operators.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn add(a:i32, b:i32) -> i32 {\n      let result = a\n      result += b\n      result\n    }\n\n    pub fn subtract(a:i32, b:i32) -> i32 {\n      let result = a\n      result -= b\n      result\n    }\n\n    pub fn multiply(a:i32, b:i32) -> i32 {\n      let result = a\n      result *= b\n      result\n    }\n\n    pub fn divide(a:i32, b:i32) -> i32 {\n      let result = a\n      result /= b\n      result\n    }\n\n    pub fn remainder(a:i32, b:i32) -> i32 {\n      let result = a\n      result %= b\n      result\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [15 x i64*]
 
 define i32 @add(i32 %0, i32 %1) {
 body:
@@ -47,10 +43,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [15 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__update_parameter.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__update_parameter.snap
index a8f372330..3ae2e78f1 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__update_parameter.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__update_parameter.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn add_three(a:i32) -> i32 {\n      a += 3;\n      a\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [2 x i64*]
 
 define i32 @add_three(i32 %0) {
 body:
@@ -23,10 +19,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [2 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__void_return.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__void_return.snap
index add5f6807..e9afcdc54 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__void_return.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__void_return.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    fn bar() {\n        let a = 3;\n    }\n    pub fn foo(a:i32) {\n        let c = bar()\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [1 x i64*]
 
 define void @bar() {
 body:
@@ -28,10 +24,5 @@ body:
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [1 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/src/snapshots/mun_codegen__test__while_expr.snap b/crates/mun_codegen/src/snapshots/mun_codegen__test__while_expr.snap
index 9ebe04ce4..d7afdb6d0 100644
--- a/crates/mun_codegen/src/snapshots/mun_codegen__test__while_expr.snap
+++ b/crates/mun_codegen/src/snapshots/mun_codegen__test__while_expr.snap
@@ -2,16 +2,12 @@
 source: crates/mun_codegen/src/test.rs
 assertion_line: 1045
 expression: "\n    pub fn foo(n:i32) {\n        while n<3 {\n            n += 1;\n        };\n\n        // This will be completely optimized out\n        while n<4 {\n            break;\n        };\n    }\n    "
-
 ---
 ; == FILE IR (mod) =====================================
 ; ModuleID = 'mod'
 source_filename = "mod"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@global_type_table = external global [1 x %"mun_codegen::ir::types::TypeInfo"*]
+@global_type_lookup_table = external global [1 x i64*]
 
 define void @foo(i32 %0) {
 body:
@@ -34,10 +30,5 @@ whilecond3:                                       ; preds = %whilecond
 ; ModuleID = 'group_name'
 source_filename = "group_name"
 
-%"mun_codegen::ir::types::TypeInfo" = type <{ [0 x i64], [16 x i8], [0 x i64], i8*, [0 x i64], i32, [0 x i64], i8, [3 x i8], %"mun_codegen::ir::types::TypeInfoData", [0 x i64] }>
-%"mun_codegen::ir::types::TypeInfoData" = type <{ [0 x i8], i8, [39 x i8] }>
-
-@"type_info::<core::i32>::name" = private unnamed_addr constant [10 x i8] c"core::i32\00"
-@"type_info::<core::i32>" = private unnamed_addr constant <{ [16 x i8], i8*, [48 x i8] }> <{ [16 x i8] c"\17yzt\19\D62\17\D25\95C\17\88[\FA", i8* getelementptr inbounds ([10 x i8], [10 x i8]* @"type_info::<core::i32>::name", i32 0, i32 0), [48 x i8] c" \00\00\00\04\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }>
-@global_type_table = constant [1 x %"mun_codegen::ir::types::TypeInfo"*] [%"mun_codegen::ir::types::TypeInfo"* @"type_info::<core::i32>"]
+@global_type_lookup_table = global [1 x i64*] zeroinitializer
 
diff --git a/crates/mun_codegen/tests/abi.rs b/crates/mun_codegen/tests/abi.rs
index 98887c9e5..03b1a4a97 100644
--- a/crates/mun_codegen/tests/abi.rs
+++ b/crates/mun_codegen/tests/abi.rs
@@ -46,7 +46,7 @@ fn test_abi_compatibility() {
     assert_eq!(module_info.num_functions, 2);
 
     let fn_def = get_function_info(module_info, fn_name);
-    test_function_args(fn_def, &[(f64::type_name(), f64::type_guid())]);
+    test_function_args(fn_def, &[(f64::type_name(), f64::type_id())]);
     test_function_return_type_some::<i32>(fn_def);
 
     let fn_def2 = get_function_info(module_info, fn_name2);
@@ -83,13 +83,13 @@ fn test_abi_compatibility() {
             .unwrap_or_else(|| panic!("Failed to retrieve function definition '{}'", fn_name))
     }
 
-    fn test_function_args(fn_def: &abi::FunctionDefinition, args: &[(&str, abi::Guid)]) {
+    fn test_function_args(fn_def: &abi::FunctionDefinition, args: &[(&str, abi::TypeId)]) {
         assert_eq!(
             usize::from(fn_def.prototype.signature.num_arg_types),
             args.len()
         );
 
-        for (idx, (arg_name, arg_guid)) in args.iter().enumerate() {
+        for (idx, (_, arg_type_id)) in args.iter().enumerate() {
             let fn_arg_type = fn_def
                 .prototype
                 .signature
@@ -102,8 +102,7 @@ fn test_abi_compatibility() {
                     )
                 });
 
-            assert_eq!(fn_arg_type.guid, *arg_guid);
-            assert_eq!(fn_arg_type.name(), *arg_name);
+            assert_eq!(fn_arg_type, arg_type_id);
         }
     }
 
@@ -123,8 +122,7 @@ fn test_abi_compatibility() {
                 fn_def.prototype.name()
             )
         });
-        assert_eq!(fn_return_type.guid, R::type_guid());
-        assert_eq!(fn_return_type.name(), R::type_name());
+        assert_eq!(fn_return_type, R::type_id());
     }
 
     fn test_struct_info<T: Sized, F: Sized + ReturnTypeReflection>(
@@ -156,8 +154,7 @@ fn test_abi_compatibility() {
             assert_eq!(lhs, *rhs);
         }
         for field_type in struct_info.field_types().iter() {
-            assert_eq!(field_type.guid, F::type_guid());
-            assert_eq!(field_type.name(), F::type_name());
+            assert_eq!(field_type.guid, F::type_id().guid);
         }
 
         let mut offset = 0;
diff --git a/crates/mun_memory/tests/diff/mod.rs b/crates/mun_memory/tests/diff/mod.rs
index 738e498ee..750242ac0 100644
--- a/crates/mun_memory/tests/diff/mod.rs
+++ b/crates/mun_memory/tests/diff/mod.rs
@@ -3,26 +3,25 @@ mod primitives;
 mod structs;
 mod util;
 
-use mun_memory::diff::{diff, Diff};
+use crate::fake_struct;
+use mun_memory::{
+    diff::{diff, Diff},
+    type_table::TypeTable,
+};
 use util::*;
 
 #[test]
 fn add() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
-    let struct1 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT1_GUID,
-        StructInfo::new(&[("a", &int), ("b", &float)]),
-    );
+    let type_table = TypeTable::default();
 
-    let old = &[&int, &struct1];
-    let new = &[&int, &struct1, &float];
+    let int = type_table.find_type_info_by_name("core::i64").unwrap();
+    let float = type_table.find_type_info_by_name("core::f64").unwrap();
+    let struct1 = fake_struct!(type_table, "struct1", "a" => i64, "b" => f64);
+
+    let old = &[int.clone(), struct1.clone()];
+    let new = &[int.clone(), struct1.clone(), float.clone()];
 
     let diff = diff(old, new);
     assert_eq!(diff, vec![Diff::Insert { index: 2 }]);
-    assert_eq!(
-        apply_diff(old, new, diff),
-        vec![int.clone(), struct1.clone(), float.clone()]
-    );
+    assert_eq!(apply_diff(old, new, diff), vec![int, struct1, float]);
 }
diff --git a/crates/mun_memory/tests/diff/primitives.rs b/crates/mun_memory/tests/diff/primitives.rs
index 5a1cf9f61..e450097c3 100644
--- a/crates/mun_memory/tests/diff/primitives.rs
+++ b/crates/mun_memory/tests/diff/primitives.rs
@@ -1,55 +1,66 @@
 use super::util::*;
-use mun_memory::diff::{diff, Diff};
+use mun_memory::{
+    diff::{diff, Diff},
+    type_table::TypeTable,
+};
 
 #[test]
 fn add() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let old = &[&int];
-    let new = &[&int, &float];
+    let int = type_table.find_type_info_by_name("core::i64").unwrap();
+    let float = type_table.find_type_info_by_name("core::f64").unwrap();
+
+    let old = &[int.clone()];
+    let new = &[int.clone(), float.clone()];
 
     let diff = diff(old, new);
     assert_eq!(diff, vec![Diff::Insert { index: 1 }]);
-    assert_eq!(apply_diff(old, new, diff), vec![int.clone(), float.clone()]);
+    assert_eq!(apply_diff(old, new, diff), vec![int, float]);
 }
 
 #[test]
 fn remove() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
+
+    let int = type_table.find_type_info_by_name("core::i64").unwrap();
+    let float = type_table.find_type_info_by_name("core::f64").unwrap();
 
-    let old = &[&int, &float];
-    let new = &[&float];
+    let old = &[int, float.clone()];
+    let new = &[float.clone()];
 
     let diff = diff(old, new);
     assert_eq!(diff, vec![Diff::Delete { index: 0 },]);
-    assert_eq!(apply_diff(old, new, diff), vec![float.clone()]);
+    assert_eq!(apply_diff(old, new, diff), vec![float]);
 }
 
 #[test]
 fn replace() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let old = &[&int];
-    let new = &[&float];
+    let int = type_table.find_type_info_by_name("core::i64").unwrap();
+    let float = type_table.find_type_info_by_name("core::f64").unwrap();
+
+    let old = &[int];
+    let new = &[float.clone()];
 
     let diff = diff(old, new);
     assert_eq!(
         diff,
         vec![Diff::Delete { index: 0 }, Diff::Insert { index: 0 }]
     );
-    assert_eq!(apply_diff(old, new, diff), vec![float.clone()]);
+    assert_eq!(apply_diff(old, new, diff), vec![float]);
 }
 
 #[test]
 fn swap() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
+
+    let int = type_table.find_type_info_by_name("core::i64").unwrap();
+    let float = type_table.find_type_info_by_name("core::f64").unwrap();
 
-    let old = &[&int, &float];
-    let new = &[&float, &int];
+    let old = &[int.clone(), float.clone()];
+    let new = &[float.clone(), int.clone()];
 
     let diff = diff(old, new);
     assert_eq!(
@@ -59,5 +70,5 @@ fn swap() {
             new_index: 1
         },]
     );
-    assert_eq!(apply_diff(old, new, diff), vec![float.clone(), int.clone()]);
+    assert_eq!(apply_diff(old, new, diff), vec![float, int]);
 }
diff --git a/crates/mun_memory/tests/diff/structs.rs b/crates/mun_memory/tests/diff/structs.rs
index 5a6392e2d..0ac8cce9b 100644
--- a/crates/mun_memory/tests/diff/structs.rs
+++ b/crates/mun_memory/tests/diff/structs.rs
@@ -1,112 +1,92 @@
-use super::util::*;
-use mun_memory::diff::{diff, Diff, FieldDiff, FieldEditKind};
+use std::sync::Arc;
 
-// TODO: Once we can generate `Guid`s based on the data layout, we can just directly check
-// `TypeInfo`s against each other.
-fn assert_eq_struct(result: &[TypeInfo], expected: &[TypeInfo]) {
+use crate::{diff::util::*, fake_struct};
+use mun_memory::{
+    diff::{diff, Diff, FieldDiff, FieldEditKind},
+    type_table::TypeTable,
+    TypeInfo,
+};
+
+fn assert_eq_struct(result: &[Arc<TypeInfo>], expected: &[Arc<TypeInfo>]) {
     assert_eq!(result.len(), expected.len());
     for (lhs, rhs) in result.iter().zip(expected.iter()) {
-        assert_eq!(lhs.layout, rhs.layout);
-        assert_eq!(lhs.data, rhs.data);
+        assert_eq!(lhs, rhs);
     }
 }
 
 #[test]
 fn add() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let struct1 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT1_GUID,
-        StructInfo::new(&[("a", &int), ("b", &float)]),
+    let struct1 = fake_struct!(type_table, "struct1",
+        "a" => i64, "b" => f64
     );
-    let struct2 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT2_GUID,
-        StructInfo::new(&[("c", &float), ("d", &int)]),
+    let struct2 = fake_struct!(type_table, "struct2",
+        "c" => f64, "d" => i64
     );
 
-    let old = &[&struct1];
-    let new = &[&struct1, &struct2];
+    let old = &[struct1.clone()];
+    let new = &[struct1.clone(), struct2.clone()];
 
     let diff = diff(old, new);
     assert_eq!(diff, vec![Diff::Insert { index: 1 }]);
-    assert_eq_struct(
-        &apply_diff(old, new, diff),
-        &[struct1.clone(), struct2.clone()],
-    );
+    assert_eq_struct(&apply_diff(old, new, diff), &[struct1, struct2]);
 }
 
 #[test]
 fn remove() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let struct1 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT1_GUID,
-        StructInfo::new(&[("a", &int), ("b", &float)]),
+    let struct1 = fake_struct!(type_table, "struct1",
+        "a" => i64, "b" => f64
     );
-    let struct2 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT2_GUID,
-        StructInfo::new(&[("c", &float), ("d", &int)]),
+    let struct2 = fake_struct!(type_table, "struct2",
+        "c" => f64, "d" => i64
     );
 
-    let old = &[&struct1, &struct2];
-    let new = &[&struct1];
+    let old = &[struct1.clone(), struct2];
+    let new = &[struct1.clone()];
 
     let diff = diff(old, new);
     assert_eq!(diff, vec![Diff::Delete { index: 1 },]);
-    assert_eq_struct(&apply_diff(old, new, diff), &[struct1.clone()]);
+    assert_eq_struct(&apply_diff(old, new, diff), &[struct1]);
 }
 
 #[test]
 fn replace() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let struct1 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT1_GUID,
-        StructInfo::new(&[("a", &int), ("b", &float)]),
+    let struct1 = fake_struct!(type_table, "struct1",
+        "a" => i64, "b" => f64
     );
-    let struct2 = TypeInfo::new_struct(
-        STRUCT2_NAME,
-        STRUCT2_GUID,
-        StructInfo::new(&[("c", &float), ("d", &int)]),
+    let struct2 = fake_struct!(type_table, "struct2",
+        "c" => f64, "d" => i64
     );
 
-    let old = &[&struct1];
-    let new = &[&struct2];
+    let old = &[struct1];
+    let new = &[struct2.clone()];
 
     let diff = diff(old, new);
     assert_eq!(
         diff,
         vec![Diff::Delete { index: 0 }, Diff::Insert { index: 0 }]
     );
-    assert_eq_struct(&apply_diff(old, new, diff), &[struct2.clone()]);
+    assert_eq_struct(&apply_diff(old, new, diff), &[struct2]);
 }
 
 #[test]
 fn swap() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let struct1 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT1_GUID,
-        StructInfo::new(&[("a", &int), ("b", &float)]),
+    let struct1 = fake_struct!(type_table, "struct1",
+        "a" => i64, "b" => f64
     );
-    let struct2 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT2_GUID,
-        StructInfo::new(&[("c", &float), ("d", &int)]),
+    let struct2 = fake_struct!(type_table, "struct2",
+        "c" => f64, "d" => i64
     );
 
-    let old = &[&struct1, &struct2];
-    let new = &[&struct2, &struct1];
+    let old = &[struct1.clone(), struct2.clone()];
+    let new = &[struct2.clone(), struct1.clone()];
 
     let diff = diff(old, new);
     assert_eq!(
@@ -116,30 +96,22 @@ fn swap() {
             new_index: 1
         }]
     );
-    assert_eq_struct(
-        &apply_diff(old, new, diff),
-        &[struct2.clone(), struct1.clone()],
-    );
+    assert_eq_struct(&apply_diff(old, new, diff), &[struct2, struct1]);
 }
 
 #[test]
 fn add_field1() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let struct1 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT1_GUID,
-        StructInfo::new(&[("b", &int), ("c", &float)]),
+    let struct1 = fake_struct!(type_table, "struct1",
+        "b" => i64, "c" => f64
     );
-    let struct2 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT2_GUID,
-        StructInfo::new(&[("a", &int), ("b", &int), ("c", &float)]),
+    let struct2 = fake_struct!(type_table, "struct1",
+        "a" => i64, "b" => i64, "c" => f64
     );
 
-    let old = &[&struct1];
-    let new = &[&struct2];
+    let old = &[struct1];
+    let new = &[struct2.clone()];
 
     let diff = diff(old, new);
     assert_eq!(
@@ -150,27 +122,22 @@ fn add_field1() {
             new_index: 0,
         }]
     );
-    assert_eq_struct(&apply_diff(old, new, diff), &[struct2.clone()]);
+    assert_eq_struct(&apply_diff(old, new, diff), &[struct2]);
 }
 
 #[test]
 fn add_field2() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let struct1 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT1_GUID,
-        StructInfo::new(&[("a", &int), ("c", &float)]),
+    let struct1 = fake_struct!(type_table, "struct1",
+        "a" => i64, "c" => f64
     );
-    let struct2 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT2_GUID,
-        StructInfo::new(&[("a", &int), ("b", &float), ("c", &float)]),
+    let struct2 = fake_struct!(type_table, "struct1",
+        "a" => i64, "b" => f64, "c" => f64
     );
 
-    let old = &[&struct1];
-    let new = &[&struct2];
+    let old = &[struct1];
+    let new = &[struct2.clone()];
 
     let diff = diff(old, new);
     assert_eq!(
@@ -181,27 +148,22 @@ fn add_field2() {
             new_index: 0,
         }]
     );
-    assert_eq_struct(&apply_diff(old, new, diff), &[struct2.clone()]);
+    assert_eq_struct(&apply_diff(old, new, diff), &[struct2]);
 }
 
 #[test]
 fn add_field3() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let struct1 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT1_GUID,
-        StructInfo::new(&[("a", &int), ("b", &float)]),
+    let struct1 = fake_struct!(type_table, "struct1",
+        "a" => i64, "b" => f64
     );
-    let struct2 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT2_GUID,
-        StructInfo::new(&[("a", &int), ("b", &float), ("c", &float)]),
+    let struct2 = fake_struct!(type_table, "struct1",
+        "a" => i64, "b" => f64, "c" => f64
     );
 
-    let old = &[&struct1];
-    let new = &[&struct2];
+    let old = &[struct1];
+    let new = &[struct2.clone()];
 
     let diff = diff(old, new);
     assert_eq!(
@@ -212,23 +174,20 @@ fn add_field3() {
             new_index: 0,
         }]
     );
-    assert_eq_struct(&apply_diff(old, new, diff), &[struct2.clone()]);
+    assert_eq_struct(&apply_diff(old, new, diff), &[struct2]);
 }
 
 #[test]
 fn remove_field1() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let struct1 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT1_GUID,
-        StructInfo::new(&[("a", &float), ("b", &float), ("c", &int)]),
+    let struct1 = fake_struct!(type_table, "struct1",
+        "a" => f64, "b" => f64, "c" => i64
     );
-    let struct2 = TypeInfo::new_struct(STRUCT1_NAME, STRUCT2_GUID, StructInfo::new(&[("c", &int)]));
+    let struct2 = fake_struct!(type_table, "struct1", "c" => i64);
 
-    let old = &[&struct1];
-    let new = &[&struct2];
+    let old = &[struct1];
+    let new = &[struct2.clone()];
 
     let diff = diff(old, new);
     assert_eq!(
@@ -242,23 +201,20 @@ fn remove_field1() {
             new_index: 0,
         }]
     );
-    assert_eq_struct(&apply_diff(old, new, diff), &[struct2.clone()]);
+    assert_eq_struct(&apply_diff(old, new, diff), &[struct2]);
 }
 
 #[test]
 fn remove_field2() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let struct1 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT1_GUID,
-        StructInfo::new(&[("a", &float), ("b", &int), ("c", &float)]),
+    let struct1 = fake_struct!(type_table, "struct1",
+        "a" => f64, "b" => i64, "c" => f64
     );
-    let struct2 = TypeInfo::new_struct(STRUCT1_NAME, STRUCT2_GUID, StructInfo::new(&[("b", &int)]));
+    let struct2 = fake_struct!(type_table, "struct1", "b" => i64);
 
-    let old = &[&struct1];
-    let new = &[&struct2];
+    let old = &[struct1];
+    let new = &[struct2.clone()];
 
     let diff = diff(old, new);
     assert_eq!(
@@ -272,23 +228,20 @@ fn remove_field2() {
             new_index: 0,
         }]
     );
-    assert_eq_struct(&apply_diff(old, new, diff), &[struct2.clone()]);
+    assert_eq_struct(&apply_diff(old, new, diff), &[struct2]);
 }
 
 #[test]
 fn remove_field3() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let struct1 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT1_GUID,
-        StructInfo::new(&[("a", &int), ("b", &float), ("c", &float)]),
+    let struct1 = fake_struct!(type_table, "struct1",
+        "a" => i64, "b" => f64, "c" => f64
     );
-    let struct2 = TypeInfo::new_struct(STRUCT1_NAME, STRUCT2_GUID, StructInfo::new(&[("a", &int)]));
+    let struct2 = fake_struct!(type_table, "struct1", "a" => i64);
 
-    let old = &[&struct1];
-    let new = &[&struct2];
+    let old = &[struct1];
+    let new = &[struct2.clone()];
 
     let diff = diff(old, new);
     assert_eq!(
@@ -302,27 +255,22 @@ fn remove_field3() {
             new_index: 0,
         }]
     );
-    assert_eq_struct(&apply_diff(old, new, diff), &[struct2.clone()]);
+    assert_eq_struct(&apply_diff(old, new, diff), &[struct2]);
 }
 
 #[test]
 fn swap_fields() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let struct1 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT1_GUID,
-        StructInfo::new(&[("a", &float), ("b", &int), ("c", &float)]),
+    let struct1 = fake_struct!(type_table, "struct1",
+        "a" => f64, "b" => i64, "c" => f64
     );
-    let struct2 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT2_GUID,
-        StructInfo::new(&[("c", &float), ("a", &float), ("b", &int)]),
+    let struct2 = fake_struct!(type_table, "struct1",
+        "c" => f64, "a" => f64, "b" => i64
     );
 
-    let old = &[&struct1];
-    let new = &[&struct2];
+    let old = &[struct1];
+    let new = &[struct2.clone()];
 
     let diff = diff(old, new);
     assert_eq!(
@@ -337,27 +285,22 @@ fn swap_fields() {
             new_index: 0,
         }]
     );
-    assert_eq_struct(&apply_diff(old, new, diff), &[struct2.clone()]);
+    assert_eq_struct(&apply_diff(old, new, diff), &[struct2]);
 }
 
 #[test]
 fn swap_fields2() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let struct1 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT1_GUID,
-        StructInfo::new(&[("a", &float), ("b", &int), ("c", &float), ("d", &int)]),
+    let struct1 = fake_struct!(type_table, "struct1",
+        "a" => f64, "b" => i64, "c" => f64, "d" => i64
     );
-    let struct2 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT2_GUID,
-        StructInfo::new(&[("d", &int), ("c", &float), ("b", &int), ("a", &float)]),
+    let struct2 = fake_struct!(type_table, "struct1",
+        "d" => i64, "c" => f64, "b" => i64, "a" => f64
     );
 
-    let old = &[&struct1];
-    let new = &[&struct2];
+    let old = &[struct1];
+    let new = &[struct2.clone()];
 
     let diff = diff(old, new);
     assert_eq!(
@@ -384,27 +327,22 @@ fn swap_fields2() {
             new_index: 0
         }]
     );
-    assert_eq_struct(&apply_diff(old, new, diff), &[struct2.clone()]);
+    assert_eq_struct(&apply_diff(old, new, diff), &[struct2]);
 }
 
 #[test]
 fn cast_field() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let struct1 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT1_GUID,
-        StructInfo::new(&[("a", &int), ("b", &float), ("c", &float)]),
+    let struct1 = fake_struct!(type_table, "struct1",
+        "a" => i64, "b" => f64, "c" => f64
     );
-    let struct2 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT2_GUID,
-        StructInfo::new(&[("a", &float), ("b", &int), ("c", &int)]),
+    let struct2 = fake_struct!(type_table, "struct1",
+        "a" => f64, "b" => i64, "c" => i64
     );
 
-    let old = &[&struct1];
-    let new = &[&struct2];
+    let old = &[struct1];
+    let new = &[struct2.clone()];
 
     let diff = diff(old, new);
     assert_eq!(
@@ -428,27 +366,22 @@ fn cast_field() {
             new_index: 0,
         }]
     );
-    assert_eq_struct(&apply_diff(old, new, diff), &[struct2.clone()]);
+    assert_eq_struct(&apply_diff(old, new, diff), &[struct2]);
 }
 
 #[test]
 fn rename_field1() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let struct1 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT1_GUID,
-        StructInfo::new(&[("a", &int), ("b", &float), ("c", &float)]),
+    let struct1 = fake_struct!(type_table, "struct1",
+        "a" => i64, "b" => f64, "c" => f64
     );
-    let struct2 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT2_GUID,
-        StructInfo::new(&[("a", &int), ("d", &float), ("c", &float)]),
+    let struct2 = fake_struct!(type_table, "struct1",
+        "a" => i64, "d" => f64, "c" => f64
     );
 
-    let old = &[&struct1];
-    let new = &[&struct2];
+    let old = &[struct1];
+    let new = &[struct2.clone()];
 
     let diff = diff(old, new);
     assert_eq!(
@@ -462,27 +395,22 @@ fn rename_field1() {
             new_index: 0,
         }]
     );
-    assert_eq_struct(&apply_diff(old, new, diff), &[struct2.clone()]);
+    assert_eq_struct(&apply_diff(old, new, diff), &[struct2]);
 }
 
 #[test]
 fn rename_field2() {
-    let int = TypeInfo::new_fundamental::<i64>();
-    let float = TypeInfo::new_fundamental::<f64>();
+    let type_table = TypeTable::default();
 
-    let struct1 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT1_GUID,
-        StructInfo::new(&[("a", &int), ("b", &float), ("c", &float)]),
+    let struct1 = fake_struct!(type_table, "struct1",
+        "a" => i64, "b" => f64, "c" => f64
     );
-    let struct2 = TypeInfo::new_struct(
-        STRUCT1_NAME,
-        STRUCT2_GUID,
-        StructInfo::new(&[("d", &int), ("e", &float), ("f", &float)]),
+    let struct2 = fake_struct!(type_table, "struct1",
+        "d" => i64, "e" => f64, "f" => f64
     );
 
-    let old = &[&struct1];
-    let new = &[&struct2];
+    let old = &[struct1];
+    let new = &[struct2.clone()];
 
     let diff = diff(old, new);
     assert_eq!(
@@ -506,5 +434,5 @@ fn rename_field2() {
             new_index: 0,
         }]
     );
-    assert_eq_struct(&apply_diff(old, new, diff), &[struct2.clone()]);
+    assert_eq_struct(&apply_diff(old, new, diff), &[struct2]);
 }
diff --git a/crates/mun_memory/tests/diff/util.rs b/crates/mun_memory/tests/diff/util.rs
index 25674b2c4..02ec2c4ca 100644
--- a/crates/mun_memory/tests/diff/util.rs
+++ b/crates/mun_memory/tests/diff/util.rs
@@ -1,137 +1,13 @@
 #![allow(dead_code)]
 use mun_memory::{
     diff::{myers, Diff, FieldDiff, FieldEditKind},
-    TypeDesc, TypeFields, TypeGroup, TypeMemory,
+    FieldInfo, TypeInfo, TypeInfoData,
 };
-use std::alloc::Layout;
+use std::sync::Arc;
 
-pub const STRUCT1_NAME: &str = "struct1";
-pub const STRUCT1_GUID: abi::Guid = abi::Guid([
-    0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150,
-]);
-pub const STRUCT2_NAME: &str = "struct2";
-pub const STRUCT2_GUID: abi::Guid = abi::Guid([
-    150, 140, 130, 120, 110, 100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 0,
-]);
+use crate::util::{fake_layout, struct_guid};
 
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct StructInfo {
-    fields: Vec<(String, TypeInfo)>,
-}
-
-impl StructInfo {
-    pub fn new(fields: &[(&str, &TypeInfo)]) -> Self {
-        Self {
-            fields: fields
-                .iter()
-                .map(|(name, ty)| (name.to_string(), (*ty).clone()))
-                .collect(),
-        }
-    }
-
-    pub fn layout(&self) -> Layout {
-        // NOTE: This implementation is naive, but it is merely a test
-        let size = self.fields.iter().map(|ty| ty.1.layout.size()).sum();
-        let align = self
-            .fields
-            .iter()
-            .map(|(_, ty)| ty.layout.align())
-            .max()
-            .unwrap_or(1);
-        unsafe { Layout::from_size_align_unchecked(size, align) }
-    }
-}
-
-#[derive(Clone, Debug)]
-pub struct TypeInfo {
-    pub name: String,
-    pub guid: abi::Guid,
-    pub layout: Layout,
-    pub data: TypeInfoData,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum TypeInfoData {
-    Primitive,
-    Struct(StructInfo),
-}
-
-impl TypeInfo {
-    pub fn new_fundamental<T: abi::HasStaticTypeInfo>() -> Self {
-        let type_info = T::type_info();
-        Self {
-            name: type_info.name().to_string(),
-            guid: type_info.guid,
-            layout: Layout::new::<T>(),
-            data: TypeInfoData::Primitive,
-        }
-    }
-
-    pub fn new_struct(name: &str, guid: abi::Guid, struct_info: StructInfo) -> Self {
-        Self {
-            name: name.to_string(),
-            guid,
-            layout: struct_info.layout(),
-            data: TypeInfoData::Struct(struct_info),
-        }
-    }
-}
-
-// TODO: Change Guid to be a hash of field names and field types. For fundamental types, their
-// singular field type (e.g. u8, i16, f32) is used. Order of fields is important!
-impl PartialEq for TypeInfo {
-    fn eq(&self, other: &Self) -> bool {
-        self.guid == other.guid
-    }
-}
-
-impl Eq for TypeInfo {}
-
-impl TypeDesc for &TypeInfo {
-    fn name(&self) -> &str {
-        &self.name
-    }
-    fn guid(&self) -> &abi::Guid {
-        &self.guid
-    }
-    fn group(&self) -> TypeGroup {
-        match self.data {
-            TypeInfoData::Primitive => TypeGroup::Primitive,
-            TypeInfoData::Struct(_) => TypeGroup::Struct,
-        }
-    }
-}
-
-impl TypeMemory for &TypeInfo {
-    fn layout(&self) -> Layout {
-        self.layout
-    }
-
-    fn is_stack_allocated(&self) -> bool {
-        // NOTE: This contrived test does not support structs
-        true
-    }
-}
-
-impl<'t> TypeFields<&'t TypeInfo> for &'t TypeInfo {
-    fn fields(&self) -> Vec<(&str, Self)> {
-        match &self.data {
-            TypeInfoData::Primitive => Vec::new(),
-            TypeInfoData::Struct(s) => s
-                .fields
-                .iter()
-                .map(|(name, ty)| (name.as_str(), ty))
-                .collect(),
-        }
-    }
-
-    fn offsets(&self) -> &[u16] {
-        // This is a stub, as we don't do any actual memory mapping
-        &[]
-    }
-}
-
-pub fn apply_myers_diff<'t, T: Copy + Eq>(old: &[T], new: &[T], diff: Vec<myers::Diff>) -> Vec<T> {
+pub fn apply_myers_diff<T: Clone + Eq>(old: &[T], new: &[T], diff: Vec<myers::Diff>) -> Vec<T> {
     let mut combined: Vec<_> = old.to_vec();
     for diff in diff.iter().rev() {
         if let myers::Diff::Delete { index } = diff {
@@ -141,18 +17,18 @@ pub fn apply_myers_diff<'t, T: Copy + Eq>(old: &[T], new: &[T], diff: Vec<myers:
     for diff in diff {
         if let myers::Diff::Insert { index } = diff {
             let value = unsafe { new.get_unchecked(index) };
-            combined.insert(index, *value);
+            combined.insert(index, value.clone());
         }
     }
     combined
 }
 
-pub(crate) fn apply_diff<'t>(
-    old: &[&TypeInfo],
-    new: &[&TypeInfo],
+pub(crate) fn apply_diff(
+    old: &[Arc<TypeInfo>],
+    new: &[Arc<TypeInfo>],
     diff: Vec<Diff>,
-) -> Vec<TypeInfo> {
-    let mut combined: Vec<TypeInfo> = old.iter().map(|ty| (*ty).clone()).collect();
+) -> Vec<Arc<TypeInfo>> {
+    let mut combined: Vec<Arc<TypeInfo>> = old.to_vec();
     for diff in diff.iter().rev() {
         match diff {
             Diff::Delete { index } => {
@@ -165,7 +41,7 @@ pub(crate) fn apply_diff<'t>(
             } => {
                 let old_ty = unsafe { combined.get_unchecked_mut(*old_index) };
                 let new_ty = unsafe { new.get_unchecked(*new_index) };
-                apply_mapping(old_ty, new_ty, diff);
+                apply_mapping(Arc::make_mut(old_ty), new_ty, diff);
             }
             Diff::Move { old_index, .. } => {
                 combined.remove(*old_index);
@@ -192,18 +68,18 @@ pub(crate) fn apply_diff<'t>(
     combined
 }
 
-fn apply_mapping<'t>(old: &mut TypeInfo, new: &TypeInfo, mapping: &[FieldDiff]) {
+fn apply_mapping(old: &mut TypeInfo, new: &TypeInfo, mapping: &[FieldDiff]) {
     if let TypeInfoData::Struct(old_struct) = &mut old.data {
         if let TypeInfoData::Struct(new_struct) = &new.data {
-            let mut combined: Vec<_> = old_struct.fields.to_vec();
+            let mut combined = old_struct.clone();
             for diff in mapping.iter().rev() {
                 match diff {
                     FieldDiff::Delete { index } => {
-                        combined.remove(*index);
+                        combined.fields.remove(*index);
                     }
 
                     FieldDiff::Move { old_index, .. } => {
-                        combined.remove(*old_index);
+                        combined.fields.remove(*old_index);
                     }
                     _ => (),
                 }
@@ -218,7 +94,7 @@ fn apply_mapping<'t>(old: &mut TypeInfo, new: &TypeInfo, mapping: &[FieldDiff])
             }
 
             // Sort elements in ascending order of their insertion indices.
-            let mut additions: Vec<(usize, _)> = mapping
+            let mut additions: Vec<(usize, FieldInfo)> = mapping
                 .iter()
                 .filter_map(|diff| match diff {
                     FieldDiff::Insert { index } => Some((
@@ -239,17 +115,13 @@ fn apply_mapping<'t>(old: &mut TypeInfo, new: &TypeInfo, mapping: &[FieldDiff])
             additions.sort_by(|a, b| a.0.cmp(&b.0));
 
             for (index, field) in additions {
-                combined.insert(index, field);
+                combined.fields.insert(index, field);
             }
 
-            fn edit_field(
-                kind: &FieldEditKind,
-                old_field: &mut (String, TypeInfo),
-                new_field: &(String, TypeInfo),
-            ) {
+            fn edit_field(kind: &FieldEditKind, old_field: &mut FieldInfo, new_field: &FieldInfo) {
                 match *kind {
-                    FieldEditKind::ConvertType => old_field.1 = new_field.1.clone(),
-                    FieldEditKind::Rename => old_field.0 = new_field.0.clone(),
+                    FieldEditKind::ConvertType => old_field.type_info = new_field.type_info.clone(),
+                    FieldEditKind::Rename => old_field.name = new_field.name.to_owned(),
                 }
             }
 
@@ -258,7 +130,7 @@ fn apply_mapping<'t>(old: &mut TypeInfo, new: &TypeInfo, mapping: &[FieldDiff])
                 match diff {
                     FieldDiff::Edit { index, kind } => edit_field(
                         kind,
-                        unsafe { combined.get_unchecked_mut(*index) },
+                        unsafe { combined.fields.get_unchecked_mut(*index) },
                         unsafe { new_struct.fields.get_unchecked(*index) },
                     ),
                     FieldDiff::Move {
@@ -267,15 +139,16 @@ fn apply_mapping<'t>(old: &mut TypeInfo, new: &TypeInfo, mapping: &[FieldDiff])
                         edit: Some(kind),
                     } => edit_field(
                         kind,
-                        unsafe { combined.get_unchecked_mut(*old_index) },
+                        unsafe { combined.fields.get_unchecked_mut(*old_index) },
                         unsafe { new_struct.fields.get_unchecked(*new_index) },
                     ),
                     _ => (),
                 }
             }
 
-            old_struct.fields = combined;
-            old.layout = old_struct.layout();
+            *old_struct = combined;
+            old.layout = fake_layout(old_struct);
+            old.id.guid = struct_guid(&old.name, old_struct);
         } else {
             unreachable!()
         }
diff --git a/crates/mun_memory/tests/gc/alloc.rs b/crates/mun_memory/tests/gc/alloc.rs
index 201060960..bdb8009ac 100644
--- a/crates/mun_memory/tests/gc/alloc.rs
+++ b/crates/mun_memory/tests/gc/alloc.rs
@@ -1,13 +1,16 @@
-use super::util::{EventAggregator, HasTypeInfo, TypeInfo};
-use mun_memory::gc::{Event, GcRootPtr, GcRuntime, MarkSweep};
+use super::util::EventAggregator;
+use mun_memory::{
+    gc::{Event, GcRootPtr, GcRuntime, MarkSweep},
+    HasStaticTypeInfo,
+};
 use std::sync::Arc;
 
 #[test]
 fn alloc() {
-    let runtime = MarkSweep::<&'static TypeInfo, EventAggregator<Event>>::default();
+    let runtime = MarkSweep::<EventAggregator<Event>>::default();
     let handle = runtime.alloc(i64::type_info());
 
-    assert!(std::ptr::eq(runtime.ptr_type(handle), i64::type_info()));
+    assert_eq!(&runtime.ptr_type(handle), i64::type_info());
 
     let mut events = runtime.observer().take_all().into_iter();
     assert_eq!(events.next(), Some(Event::Allocation(handle)));
@@ -16,7 +19,7 @@ fn alloc() {
 
 #[test]
 fn collect_simple() {
-    let runtime = MarkSweep::<&'static TypeInfo, EventAggregator<Event>>::default();
+    let runtime = MarkSweep::<EventAggregator<Event>>::default();
     let handle = runtime.alloc(i64::type_info());
 
     runtime.collect();
@@ -31,7 +34,7 @@ fn collect_simple() {
 
 #[test]
 fn collect_rooted() {
-    let runtime = Arc::new(MarkSweep::<&'static TypeInfo, EventAggregator<Event>>::default());
+    let runtime = Arc::new(MarkSweep::<EventAggregator<Event>>::default());
 
     // Allocate simple object and rooted object
     let handle = runtime.alloc(i64::type_info());
diff --git a/crates/mun_memory/tests/gc/structs.rs b/crates/mun_memory/tests/gc/structs.rs
index 7f267be8b..18541787d 100644
--- a/crates/mun_memory/tests/gc/structs.rs
+++ b/crates/mun_memory/tests/gc/structs.rs
@@ -1,33 +1,42 @@
-use super::util::{EventAggregator, HasTypeInfo, Trace, TypeInfo};
-use crate::{assert_variant, impl_struct_ty};
-use mun_memory::gc::{Event, GcPtr, GcRootPtr, GcRuntime, HasIndirectionPtr, MarkSweep, TypeTrace};
+use super::util::{EventAggregator, Trace};
+use crate::{assert_variant, fake_struct};
+use mun_memory::{
+    gc::{Event, GcPtr, GcRootPtr, GcRuntime, HasIndirectionPtr, MarkSweep, TypeTrace},
+    type_table::TypeTable,
+};
 use std::sync::Arc;
 
-struct Foo {
+struct FooObject {
     bar: GcPtr,
 }
 
-impl Trace for Foo {
+impl Trace for FooObject {
     fn trace(&self, handles: &mut Vec<GcPtr>) {
         handles.push(self.bar)
     }
 }
 
-impl_struct_ty!(Foo);
-
 #[test]
 fn test_trace() {
-    let runtime = MarkSweep::<&'static TypeInfo, EventAggregator<Event>>::default();
-    let mut foo_handle = runtime.alloc(Foo::type_info());
-    let bar_handle = runtime.alloc(i64::type_info());
+    let mut type_table = TypeTable::default();
+
+    let bar_type_info = fake_struct!(type_table, "core::Bar", "a" => i64);
+    type_table.insert_type(bar_type_info.clone());
+
+    let foo_type_info = fake_struct!(type_table, "core::Foo", "bar" => Bar);
+    type_table.insert_type(foo_type_info.clone());
+
+    let runtime = MarkSweep::<EventAggregator<Event>>::default();
+    let mut foo_handle = runtime.alloc(&foo_type_info);
+    let bar_handle = runtime.alloc(&bar_type_info);
 
     // Assign bar to foo.bar
     unsafe {
-        (*foo_handle.deref_mut::<Foo>()).bar = bar_handle;
+        (*foo_handle.deref_mut::<FooObject>()).bar = bar_handle;
     }
 
     // Trace foo to see if we get bar back
-    let mut trace = Foo::type_info().trace(foo_handle);
+    let mut trace = foo_type_info.trace(foo_handle);
 
     assert_eq!(trace.next(), Some(bar_handle));
     assert_eq!(trace.next(), None)
@@ -35,20 +44,28 @@ fn test_trace() {
 
 #[test]
 fn trace_collect() {
-    let runtime = Arc::new(MarkSweep::<&'static TypeInfo, EventAggregator<Event>>::default());
-    let mut foo = GcRootPtr::new(&runtime, runtime.alloc(Foo::type_info()));
-    let bar = runtime.alloc(i64::type_info());
+    let mut type_table = TypeTable::default();
+
+    let bar_type_info = fake_struct!(type_table, "core::Bar", "a" => i64);
+    type_table.insert_type(bar_type_info.clone());
+
+    let foo_type_info = fake_struct!(type_table, "core::Foo", "bar" => Bar);
+    type_table.insert_type(foo_type_info.clone());
+
+    let runtime = Arc::new(MarkSweep::<EventAggregator<Event>>::default());
+    let mut foo_ptr = GcRootPtr::new(&runtime, runtime.alloc(&foo_type_info));
+    let bar = runtime.alloc(&bar_type_info);
 
     // Assign bar to foo.bar
     unsafe {
-        (*foo.deref_mut::<Foo>()).bar = bar;
+        (*foo_ptr.deref_mut::<FooObject>()).bar = bar;
     }
 
     // Collect garbage, bar should not be collected
     runtime.collect();
 
     // Drop foo
-    let foo = foo.unroot();
+    let foo = foo_ptr.unroot();
 
     // Collect garbage, both foo and bar should be collected
     runtime.collect();
@@ -67,19 +84,27 @@ fn trace_collect() {
 
 #[test]
 fn trace_cycle() {
-    let runtime = Arc::new(MarkSweep::<&'static TypeInfo, EventAggregator<Event>>::default());
-    let mut foo = GcRootPtr::new(&runtime, runtime.alloc(Foo::type_info()));
+    let mut type_table = TypeTable::default();
 
-    // Assign bar to foo.bar
+    let bar_type_info = fake_struct!(type_table, "core::Bar", "a" => i64);
+    type_table.insert_type(bar_type_info);
+
+    let foo_type_info = fake_struct!(type_table, "core::Foo", "bar" => Bar);
+    type_table.insert_type(foo_type_info.clone());
+
+    let runtime = Arc::new(MarkSweep::<EventAggregator<Event>>::default());
+    let mut foo_ptr = GcRootPtr::new(&runtime, runtime.alloc(&foo_type_info));
+
+    // Assign foo to foo.bar
     unsafe {
-        (*foo.deref_mut::<Foo>()).bar = foo.handle();
+        (*foo_ptr.deref_mut::<FooObject>()).bar = foo_ptr.handle();
     }
 
     // Collect garbage, nothing should be collected since foo is rooted
     runtime.collect();
 
     // Drop foo
-    let foo = foo.unroot();
+    let foo = foo_ptr.unroot();
 
     // Collect garbage, foo should be collected
     runtime.collect();
diff --git a/crates/mun_memory/tests/gc/util.rs b/crates/mun_memory/tests/gc/util.rs
index 769518ca3..c886b0268 100644
--- a/crates/mun_memory/tests/gc/util.rs
+++ b/crates/mun_memory/tests/gc/util.rs
@@ -2,101 +2,12 @@
 
 use mun_memory::gc::{self, GcPtr};
 use parking_lot::Mutex;
-use std::alloc::Layout;
-
-pub struct TypeInfo {
-    pub size: usize,
-    pub alignment: usize,
-    pub tracer: Option<&'static fn(handle: GcPtr) -> Vec<GcPtr>>,
-}
 
 pub trait Trace {
     /// Called to collect all GC handles in the type
     fn trace(&self, handles: &mut Vec<GcPtr>);
 }
 
-pub trait HasTypeInfo {
-    fn type_info() -> &'static TypeInfo;
-}
-
-macro_rules! impl_primitive_types {
-    ($(
-        $ty:ident
-    ),+) => {
-        $(
-            paste::item! {
-                #[allow(non_upper_case_globals)]
-                static [<TYPE_ $ty>]: TypeInfo = TypeInfo {
-                    size: std::mem::size_of::<$ty>(),
-                    alignment: std::mem::align_of::<$ty>(),
-                    tracer: None
-                };
-
-                impl HasTypeInfo for $ty {
-                    fn type_info() -> &'static TypeInfo {
-                        &[<TYPE_ $ty>]
-                    }
-                }
-            }
-        )+
-    }
-}
-
-#[macro_export]
-macro_rules! impl_struct_ty {
-    ($ty:ident) => {
-        paste::item! {
-            #[allow(non_upper_case_globals, non_snake_case)]
-            fn [<trace_ $ty>](obj:GcPtr) -> Vec<GcPtr> {
-                let mut result = Vec::new();
-                let foo = unsafe { &(*obj.deref::<$ty>()) };
-                foo.trace(&mut result);
-                result
-            }
-
-            #[allow(non_upper_case_globals)]
-            static [<TYPE_ $ty>]: TypeInfo = TypeInfo {
-                size: std::mem::size_of::<$ty>(),
-                alignment: std::mem::align_of::<$ty>(),
-                tracer: Some(&([<trace_ $ty>] as fn(handle: GcPtr) -> Vec<GcPtr>))
-            };
-
-            impl HasTypeInfo for $ty {
-                fn type_info() -> &'static TypeInfo {
-                    &[<TYPE_ $ty>]
-                }
-            }
-        }
-    };
-}
-
-impl_primitive_types!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64, bool);
-
-impl mun_memory::TypeMemory for &'static TypeInfo {
-    fn layout(&self) -> Layout {
-        Layout::from_size_align(self.size as usize, self.alignment as usize)
-            .expect("invalid layout specified by TypeInfo")
-    }
-
-    fn is_stack_allocated(&self) -> bool {
-        // NOTE: This contrived test does not support structs
-        true
-    }
-}
-
-impl gc::TypeTrace for &'static TypeInfo {
-    type Trace = <Vec<GcPtr> as IntoIterator>::IntoIter;
-
-    fn trace(&self, obj: GcPtr) -> Self::Trace {
-        let handles = if let Some(tracer) = self.tracer {
-            tracer(obj)
-        } else {
-            Vec::new()
-        };
-        handles.into_iter()
-    }
-}
-
 pub struct EventAggregator<T: Sync + Send + Sized> {
     events: Mutex<Vec<T>>,
 }
diff --git a/crates/mun_memory/tests/tests.rs b/crates/mun_memory/tests/tests.rs
index ab11c05cf..350bca220 100644
--- a/crates/mun_memory/tests/tests.rs
+++ b/crates/mun_memory/tests/tests.rs
@@ -1,2 +1,4 @@
 mod diff;
 mod gc;
+#[macro_use]
+mod util;
diff --git a/crates/mun_memory/tests/util.rs b/crates/mun_memory/tests/util.rs
new file mode 100644
index 000000000..c1d2d76ed
--- /dev/null
+++ b/crates/mun_memory/tests/util.rs
@@ -0,0 +1,103 @@
+#![allow(dead_code)]
+use mun_memory::{StructInfo, TypeInfo};
+use std::alloc::Layout;
+
+/// Generates the Guid string for the struct.
+///
+/// This does not support recursive struct types!
+pub(crate) fn struct_guid(name: &str, struct_info: &StructInfo) -> abi::Guid {
+    let guid_string = struct_guid_string(name, struct_info);
+    abi::Guid::from(guid_string.as_bytes())
+}
+
+fn struct_guid_string(name: &str, struct_info: &StructInfo) -> String {
+    let fields: Vec<String> = struct_info
+        .fields
+        .iter()
+        .map(|field| {
+            let ty_string = type_guid_string(&field.type_info);
+            format!("{}: {}", field.name, ty_string)
+        })
+        .collect();
+
+    format!(
+        "struct {name}{{{fields}}}",
+        name = name,
+        fields = fields.join(",")
+    )
+}
+
+fn type_guid_string(type_info: &TypeInfo) -> String {
+    match &type_info.data {
+        mun_memory::TypeInfoData::Primitive => type_info.name.clone(),
+        mun_memory::TypeInfoData::Struct(s) => {
+            if s.memory_kind == abi::StructMemoryKind::Gc {
+                format!("struct {}", type_info.name)
+            } else {
+                struct_guid_string(&type_info.name, s)
+            }
+        }
+    }
+}
+
+pub(crate) fn fake_layout(struct_info: &StructInfo) -> Layout {
+    let size = struct_info
+        .fields
+        .iter()
+        .map(|field| field.type_info.layout.size())
+        .sum();
+
+    let alignment = struct_info
+        .fields
+        .iter()
+        .map(|field| field.type_info.layout.align())
+        .max()
+        .unwrap();
+
+    Layout::from_size_align(size, alignment).unwrap()
+}
+
+#[macro_export]
+macro_rules! fake_struct {
+    ($type_table:expr, $struct_name:expr, $($field_name:expr => $field_ty:ident),+) => {{
+        let mut field_names = Vec::new();
+        let mut field_types = Vec::new();
+
+        $(
+            field_names.push(String::from($field_name));
+            field_types.push($type_table.find_type_info_by_name(format!("core::{}", stringify!($field_ty))).unwrap());
+        )+
+
+        let mut total_size = 0;
+        let field_offsets: Vec<u16> = field_types
+            .iter()
+            .map(|ty| {
+                let offset = total_size;
+                total_size += ty.layout.size();
+                offset as u16
+            })
+            .collect();
+
+        let fields = itertools::izip!(field_names, field_types, field_offsets)
+            .map(|(name, type_info, offset)| mun_memory::FieldInfo {
+                name,
+                type_info,
+                offset,
+            })
+            .collect();
+
+        let struct_info = mun_memory::StructInfo {
+            fields,
+            memory_kind: abi::StructMemoryKind::Gc,
+        };
+
+        let name = String::from($struct_name);
+
+        std::sync::Arc::new(mun_memory::TypeInfo {
+            id: crate::util::struct_guid(&name, &struct_info).into(),
+            name,
+            layout: crate::util::fake_layout(&struct_info),
+            data: mun_memory::TypeInfoData::Struct(struct_info),
+        })
+    }};
+}
diff --git a/crates/mun_runtime/tests/marshalling.rs b/crates/mun_runtime/tests/marshalling.rs
index 573ce24fd..b72f08390 100644
--- a/crates/mun_runtime/tests/marshalling.rs
+++ b/crates/mun_runtime/tests/marshalling.rs
@@ -239,64 +239,64 @@ fn true_is_true() {
     assert_invoke_eq!(bool, false, driver, "test_false");
 }
 
-#[test]
-fn compiler_valid_utf8() {
-    use std::ffi::CStr;
-    use std::slice;
-
-    let driver = CompileAndRunTestDriver::new(
-        r#"
-    pub struct Foo {
-        a: i32,
-    }
-
-    pub fn foo(n:Foo)->bool { false }
-    "#,
-        |builder| builder,
-    )
-    .expect("Failed to build test driver");
-
-    let foo_func = driver.runtime.get_function_definition("foo").unwrap();
-    assert_eq!(
-        unsafe { CStr::from_ptr(foo_func.prototype.name) }
-            .to_str()
-            .is_ok(),
-        true
-    );
-
-    for arg_type in foo_func.prototype.signature.arg_types() {
-        assert_eq!(
-            unsafe { CStr::from_ptr(arg_type.name) }.to_str().is_ok(),
-            true
-        );
-
-        if let Some(s) = arg_type.as_struct() {
-            let field_names = unsafe { slice::from_raw_parts(s.field_names, s.num_fields()) };
-
-            for field_name in field_names {
-                assert_eq!(
-                    unsafe { CStr::from_ptr(*field_name) }.to_str().is_ok(),
-                    true
-                );
-            }
-        }
-    }
-    assert_eq!(
-        unsafe {
-            CStr::from_ptr(
-                foo_func
-                    .prototype
-                    .signature
-                    .return_type()
-                    .expect("Missing return type")
-                    .name,
-            )
-        }
-        .to_str()
-        .is_ok(),
-        true
-    );
-}
+// #[test]
+// fn compiler_valid_utf8() {
+//     use std::ffi::CStr;
+//     use std::slice;
+//
+//     let driver = CompileAndRunTestDriver::new(
+//         r#"
+//     pub struct Foo {
+//         a: i32,
+//     }
+//
+//     pub fn foo(n:Foo)->bool { false }
+//     "#,
+//         |builder| builder,
+//     )
+//     .expect("Failed to build test driver");
+//
+//     let foo_func = driver.runtime.get_function_definition("foo").unwrap();
+//     assert_eq!(
+//         unsafe { CStr::from_ptr(foo_func.prototype.name) }
+//             .to_str()
+//             .is_ok(),
+//         true
+//     );
+//
+//     for arg_type in foo_func.prototype.signature.arg_types() {
+//         assert_eq!(
+//             unsafe { CStr::from_ptr(arg_type.name) }.to_str().is_ok(),
+//             true
+//         );
+//
+//         if let Some(s) = arg_type.as_struct() {
+//             let field_names = unsafe { slice::from_raw_parts(s.field_names, s.num_fields()) };
+//
+//             for field_name in field_names {
+//                 assert_eq!(
+//                     unsafe { CStr::from_ptr(*field_name) }.to_str().is_ok(),
+//                     true
+//                 );
+//             }
+//         }
+//     }
+//     assert_eq!(
+//         unsafe {
+//             CStr::from_ptr(
+//                 foo_func
+//                     .prototype
+//                     .signature
+//                     .return_type()
+//                     .expect("Missing return type")
+//                     .name,
+//             )
+//         }
+//         .to_str()
+//         .is_ok(),
+//         true
+//     );
+// }
 
 #[test]
 fn fields() {
@@ -410,8 +410,9 @@ fn marshal_struct() {
             .type_info()
             .as_struct()
             .unwrap()
-            .field_names()
-            .map(|n| n.to_string())
+            .fields
+            .iter()
+            .map(|field| field.name.clone())
             .collect();
 
         let int_value = c2.get::<i64>(&field_names[0]);
diff --git a/crates/mun_runtime/tests/memory.rs b/crates/mun_runtime/tests/memory.rs
index 7422afad9..eb665d316 100644
--- a/crates/mun_runtime/tests/memory.rs
+++ b/crates/mun_runtime/tests/memory.rs
@@ -1,5 +1,6 @@
 use mun_runtime::StructRef;
 use mun_test::CompileAndRunTestDriver;
+use std::sync::Arc;
 
 #[macro_use]
 mod util;
@@ -948,3 +949,64 @@ fn insert_struct() {
     let d = foo.as_ref(&driver.runtime).get::<StructRef>("d").unwrap();
     assert_eq!(d.get::<f64>("0"), Ok(0.0));
 }
+
+#[test]
+fn test_type_table() {
+    let driver = CompileAndRunTestDriver::from_fixture(
+        r#"
+    //- /mun.toml
+    [package]
+    name="foo"
+    version="0.0.0"
+
+    //- /src/mod.mun
+
+    //- /src/foo.mun
+    use package::bar::Bar;
+
+    pub struct Foo {
+        bar: Bar
+    }
+
+    pub fn new_foo() -> Foo {
+        Foo {
+            bar: Bar {value: 3}
+        }
+    }
+
+    //- /src/bar.mun
+    pub struct Bar {
+        value: i32
+    }
+    "#,
+        |builder| builder,
+    )
+    .expect("Failed to build test driver");
+
+    let a: StructRef = driver
+        .runtime
+        .invoke("foo::new_foo", ())
+        .expect("failed to call 'new_foo'");
+
+    // Get the type of the Bar struct
+    let bar_type = driver
+        .runtime
+        .get_type_info_by_name("bar::Bar")
+        .expect("could not find Bar type");
+
+    // Get the type of the `bar` field of `Foo`.
+    let foo_bar_field_type = a
+        .type_info()
+        .as_struct()
+        .expect("is not a struct?")
+        .find_field_by_name("bar")
+        .expect("could not find `bar` field")
+        .type_info
+        .clone();
+
+    // These types should be equal
+    assert_eq!(foo_bar_field_type, bar_type);
+
+    // In fact the pointers should be equal!
+    assert!(Arc::ptr_eq(&foo_bar_field_type, &bar_type));
+}
diff --git a/crates/mun_runtime_capi/src/test_util.rs b/crates/mun_runtime_capi/src/test_util.rs
new file mode 100644
index 000000000..9c8c02966
--- /dev/null
+++ b/crates/mun_runtime_capi/src/test_util.rs
@@ -0,0 +1,88 @@
+use compiler::{Config, DisplayColor, Driver, PathOrInline, RelativePathBuf};
+use std::{ffi::CString, io::stderr, path::Path, ptr};
+
+use crate::runtime::{mun_runtime_create, mun_runtime_destroy, RuntimeHandle, RuntimeOptions};
+
+/// Combines a compiler and runtime in one. Use of the TestDriver allows for quick testing of Mun
+/// constructs in the runtime with hot-reloading support.
+pub(crate) struct TestDriver {
+    _temp_dir: tempfile::TempDir,
+    pub(crate) runtime: RuntimeHandle,
+}
+
+impl TestDriver {
+    /// Constructs a new `TestDriver` from Mun source
+    pub fn new(text: &str) -> Self {
+        let temp_dir = tempfile::TempDir::new().unwrap();
+        let config = Config {
+            out_dir: Some(temp_dir.path().to_path_buf()),
+            ..Config::default()
+        };
+        let input = PathOrInline::Inline {
+            rel_path: RelativePathBuf::from("mod.mun"),
+            contents: text.to_owned(),
+        };
+        let (mut driver, file_id) = Driver::with_file(config, input).unwrap();
+        if driver
+            .emit_diagnostics(&mut stderr(), DisplayColor::Disable)
+            .unwrap()
+        {
+            panic!("compiler errors..")
+        }
+        let out_path = driver.assembly_output_path_from_file(file_id);
+        driver.write_all_assemblies(false).unwrap();
+        let runtime = make_runtime(&out_path);
+        TestDriver {
+            _temp_dir: temp_dir,
+            runtime,
+        }
+    }
+}
+
+impl Drop for TestDriver {
+    fn drop(&mut self) {
+        mun_runtime_destroy(self.runtime);
+    }
+}
+
+fn make_runtime(lib_path: &Path) -> RuntimeHandle {
+    let lib_path = lib_path.to_str().expect("Invalid lib path");
+    let lib_path = CString::new(lib_path).unwrap();
+
+    let mut handle = RuntimeHandle(ptr::null_mut());
+    let error = unsafe {
+        mun_runtime_create(
+            lib_path.as_ptr(),
+            RuntimeOptions::default(),
+            &mut handle as *mut _,
+        )
+    };
+    assert_eq!(error.0, ptr::null(), "Failed to create runtime");
+    handle
+}
+
+#[macro_export]
+macro_rules! test_invalid_runtime {
+    ($(
+        $name:ident($($arg:expr),*)
+    ),+) => {
+        $(
+            paste::item! {
+                #[test]
+                fn [<test_ $name _invalid_runtime>]() {
+                    let runtime = RuntimeHandle(ptr::null_mut());
+                    let handle =
+                        unsafe { [<mun_ $name>](runtime $(, $arg)*) };
+
+                    let message = unsafe { CStr::from_ptr(handle.0) };
+                    assert_eq!(
+                        message.to_str().unwrap(),
+                        "Invalid argument: 'runtime' is null pointer."
+                    );
+
+                    unsafe { mun_error_destroy(handle) };
+                }
+            }
+        )+
+    };
+}
diff --git a/crates/mun_runtime_capi/src/tests.rs b/crates/mun_runtime_capi/src/tests.rs
deleted file mode 100644
index 542ad2f67..000000000
--- a/crates/mun_runtime_capi/src/tests.rs
+++ /dev/null
@@ -1,549 +0,0 @@
-use crate::{error::*, gc::*, *};
-use compiler::{Config, DisplayColor, Driver, PathOrInline, RelativePathBuf};
-use memory::gc::{GcPtr, HasIndirectionPtr, RawGcPtr};
-use runtime::UnsafeTypeInfo;
-use std::{
-    ffi::CString,
-    io::stderr,
-    mem::{self, MaybeUninit},
-    path::Path,
-    ptr::{self, NonNull},
-};
-
-/// Combines a compiler and runtime in one. Use of the TestDriver allows for quick testing of Mun
-/// constructs in the runtime with hot-reloading support.
-struct TestDriver {
-    _temp_dir: tempfile::TempDir,
-    runtime: RuntimeHandle,
-}
-
-impl TestDriver {
-    /// Constructs a new `TestDriver` from Mun source
-    fn new(text: &str) -> Self {
-        let temp_dir = tempfile::TempDir::new().unwrap();
-        let config = Config {
-            out_dir: Some(temp_dir.path().to_path_buf()),
-            ..Config::default()
-        };
-        let input = PathOrInline::Inline {
-            rel_path: RelativePathBuf::from("mod.mun"),
-            contents: text.to_owned(),
-        };
-        let (mut driver, file_id) = Driver::with_file(config, input).unwrap();
-        if driver
-            .emit_diagnostics(&mut stderr(), DisplayColor::Disable)
-            .unwrap()
-        {
-            panic!("compiler errors..")
-        }
-        let out_path = driver.assembly_output_path_from_file(file_id);
-        driver.write_all_assemblies(false).unwrap();
-        let runtime = make_runtime(&out_path);
-        TestDriver {
-            _temp_dir: temp_dir,
-            runtime,
-        }
-    }
-}
-
-impl Drop for TestDriver {
-    fn drop(&mut self) {
-        mun_runtime_destroy(self.runtime);
-    }
-}
-
-fn make_runtime(lib_path: &Path) -> RuntimeHandle {
-    let lib_path = lib_path.to_str().expect("Invalid lib path");
-    let lib_path = CString::new(lib_path).unwrap();
-
-    let mut handle = RuntimeHandle(ptr::null_mut());
-    let error = unsafe {
-        mun_runtime_create(
-            lib_path.as_ptr(),
-            RuntimeOptions::default(),
-            &mut handle as *mut _,
-        )
-    };
-    assert_eq!(error.token(), 0, "Failed to create runtime");
-    handle
-}
-
-macro_rules! test_invalid_runtime {
-    ($(
-        $name:ident($($arg:expr),*)
-    ),+) => {
-        $(
-            paste::item! {
-                #[test]
-                fn [<test_ $name _invalid_runtime>]() {
-                    let runtime = RuntimeHandle(ptr::null_mut());
-                    let handle =
-                        unsafe { [<mun_ $name>](runtime $(, $arg)*) };
-
-                    let message = unsafe { CStr::from_ptr(mun_error_message(handle)) };
-                    assert_eq!(
-                        message.to_str().unwrap(),
-                        "Invalid argument: 'runtime' is null pointer."
-                    );
-
-                    unsafe { mun_destroy_string(message.as_ptr()) };
-                }
-            }
-        )+
-    };
-}
-
-test_invalid_runtime!(
-    runtime_get_function_definition(ptr::null(), ptr::null_mut(), ptr::null_mut()),
-    runtime_update(ptr::null_mut()),
-    gc_alloc(UnsafeTypeInfo::new(NonNull::dangling()), ptr::null_mut()),
-    gc_ptr_type(mem::zeroed::<GcPtr>(), ptr::null_mut()),
-    gc_root(mem::zeroed::<GcPtr>()),
-    gc_unroot(mem::zeroed::<GcPtr>()),
-    gc_collect(ptr::null_mut())
-);
-
-#[test]
-fn test_runtime_create_invalid_lib_path() {
-    let handle =
-        unsafe { mun_runtime_create(ptr::null(), RuntimeOptions::default(), ptr::null_mut()) };
-    assert_ne!(handle.token(), 0);
-
-    let message = unsafe { CStr::from_ptr(mun_error_message(handle)) };
-    assert_eq!(
-        message.to_str().unwrap(),
-        "Invalid argument: 'library_path' is null pointer."
-    );
-
-    unsafe { mun_destroy_string(message.as_ptr()) };
-}
-
-#[test]
-fn test_runtime_create_invalid_lib_path_encoding() {
-    let invalid_encoding = ['�', '\0'];
-
-    let handle = unsafe {
-        mun_runtime_create(
-            invalid_encoding.as_ptr() as *const _,
-            RuntimeOptions::default(),
-            ptr::null_mut(),
-        )
-    };
-    assert_ne!(handle.token(), 0);
-
-    let message = unsafe { CStr::from_ptr(mun_error_message(handle)) };
-    assert_eq!(
-        message.to_str().unwrap(),
-        "Invalid argument: 'library_path' is not UTF-8 encoded."
-    );
-
-    unsafe { mun_destroy_string(message.as_ptr()) };
-}
-
-#[test]
-fn test_runtime_create_invalid_handle() {
-    let lib_path = CString::new("some/path").expect("Invalid library path");
-
-    let handle = unsafe {
-        mun_runtime_create(
-            lib_path.into_raw(),
-            RuntimeOptions::default(),
-            ptr::null_mut(),
-        )
-    };
-    assert_ne!(handle.token(), 0);
-
-    let message = unsafe { CStr::from_ptr(mun_error_message(handle)) };
-    assert_eq!(
-        message.to_str().unwrap(),
-        "Invalid argument: 'handle' is null pointer."
-    );
-
-    unsafe { mun_destroy_string(message.as_ptr()) };
-}
-
-#[test]
-fn test_runtime_get_function_info_invalid_fn_name() {
-    let driver = TestDriver::new(
-        r#"
-        pub fn main() -> i32 { 3 }
-    "#,
-    );
-
-    let handle = unsafe {
-        mun_runtime_get_function_definition(
-            driver.runtime,
-            ptr::null(),
-            ptr::null_mut(),
-            ptr::null_mut(),
-        )
-    };
-
-    let message = unsafe { CStr::from_ptr(mun_error_message(handle)) };
-    assert_eq!(
-        message.to_str().unwrap(),
-        "Invalid argument: 'fn_name' is null pointer."
-    );
-
-    unsafe { mun_destroy_string(message.as_ptr()) };
-}
-
-#[test]
-fn test_runtime_get_function_info_invalid_fn_name_encoding() {
-    let driver = TestDriver::new(
-        r#"
-        pub fn main() -> i32 { 3 }
-    "#,
-    );
-
-    let invalid_encoding = ['�', '\0'];
-    let handle = unsafe {
-        mun_runtime_get_function_definition(
-            driver.runtime,
-            invalid_encoding.as_ptr() as *const _,
-            ptr::null_mut(),
-            ptr::null_mut(),
-        )
-    };
-
-    let message = unsafe { CStr::from_ptr(mun_error_message(handle)) };
-    assert_eq!(
-        message.to_str().unwrap(),
-        "Invalid argument: 'fn_name' is not UTF-8 encoded."
-    );
-
-    unsafe { mun_destroy_string(message.as_ptr()) };
-}
-
-#[test]
-fn test_runtime_get_function_info_invalid_has_fn_info() {
-    let driver = TestDriver::new(
-        r#"
-        pub fn main() -> i32 { 3 }
-    "#,
-    );
-
-    let fn_name = CString::new("main").expect("Invalid function name");
-    let handle = unsafe {
-        mun_runtime_get_function_definition(
-            driver.runtime,
-            fn_name.as_ptr(),
-            ptr::null_mut(),
-            ptr::null_mut(),
-        )
-    };
-
-    let message = unsafe { CStr::from_ptr(mun_error_message(handle)) };
-    assert_eq!(
-        message.to_str().unwrap(),
-        "Invalid argument: 'has_fn_info' is null pointer."
-    );
-
-    unsafe { mun_destroy_string(message.as_ptr()) };
-}
-
-#[test]
-fn test_runtime_get_function_info_invalid_fn_info() {
-    let driver = TestDriver::new(
-        r#"
-        pub fn main() -> i32 { 3 }
-    "#,
-    );
-
-    let fn_name = CString::new("main").expect("Invalid function name");
-    let mut has_fn_info = false;
-    let handle = unsafe {
-        mun_runtime_get_function_definition(
-            driver.runtime,
-            fn_name.as_ptr(),
-            &mut has_fn_info as *mut _,
-            ptr::null_mut(),
-        )
-    };
-
-    let message = unsafe { CStr::from_ptr(mun_error_message(handle)) };
-    assert_eq!(
-        message.to_str().unwrap(),
-        "Invalid argument: 'fn_info' is null pointer."
-    );
-
-    unsafe { mun_destroy_string(message.as_ptr()) };
-}
-
-#[test]
-fn test_runtime_get_function_info() {
-    let driver = TestDriver::new(
-        r#"
-        pub fn main() -> i32 { 3 }
-    "#,
-    );
-
-    let fn_name = CString::new("main").expect("Invalid function name");
-    let mut has_fn_info = false;
-    let mut fn_definition = MaybeUninit::uninit();
-    let handle = unsafe {
-        mun_runtime_get_function_definition(
-            driver.runtime,
-            fn_name.as_ptr(),
-            &mut has_fn_info as *mut _,
-            fn_definition.as_mut_ptr(),
-        )
-    };
-    assert_eq!(handle.token(), 0);
-    assert!(has_fn_info);
-    let _fn_definition = unsafe { fn_definition.assume_init() };
-}
-
-#[test]
-fn test_runtime_update_invalid_updated() {
-    let driver = TestDriver::new(
-        r#"
-        pub fn main() -> i32 { 3 }
-    "#,
-    );
-
-    let handle = unsafe { mun_runtime_update(driver.runtime, ptr::null_mut()) };
-
-    let message = unsafe { CStr::from_ptr(mun_error_message(handle)) };
-    assert_eq!(
-        message.to_str().unwrap(),
-        "Invalid argument: 'updated' is null pointer."
-    );
-
-    unsafe { mun_destroy_string(message.as_ptr()) };
-}
-
-#[test]
-fn test_runtime_update() {
-    let driver = TestDriver::new(
-        r#"
-        pub fn main() -> i32 { 3 }
-    "#,
-    );
-
-    let mut updated = false;
-    let handle = unsafe { mun_runtime_update(driver.runtime, &mut updated as *mut _) };
-    assert_eq!(handle.token(), 0);
-}
-
-#[test]
-fn test_gc_alloc_invalid_obj() {
-    let driver = TestDriver::new(
-        r#"
-        pub struct Foo;
-
-        pub fn main() -> Foo { Foo }
-    "#,
-    );
-    let fn_name = CString::new("main").expect("Invalid function name");
-    let mut has_fn_info = false;
-    let mut fn_definition = MaybeUninit::uninit();
-    let handle = unsafe {
-        mun_runtime_get_function_definition(
-            driver.runtime,
-            fn_name.as_ptr(),
-            &mut has_fn_info as *mut _,
-            fn_definition.as_mut_ptr(),
-        )
-    };
-    assert_eq!(handle.token(), 0);
-
-    let fn_definition = unsafe { fn_definition.assume_init() };
-    // TODO: Simplify this once we have `mun_runtime_find_type_info`
-    let return_type = fn_definition.prototype.signature.return_type().unwrap();
-    let return_type =
-        UnsafeTypeInfo::new(NonNull::new(return_type as *const abi::TypeInfo as *mut _).unwrap());
-
-    let handle = unsafe { mun_gc_alloc(driver.runtime, return_type, ptr::null_mut()) };
-    let message = unsafe { CStr::from_ptr(mun_error_message(handle)) };
-    assert_eq!(
-        message.to_str().unwrap(),
-        "Invalid argument: 'obj' is null pointer."
-    );
-
-    unsafe { mun_destroy_string(message.as_ptr()) };
-}
-
-#[test]
-fn test_gc_alloc() {
-    let driver = TestDriver::new(
-        r#"
-        pub struct Foo;
-
-        pub fn main() -> Foo { Foo }
-    "#,
-    );
-    let fn_name = CString::new("main").expect("Invalid function name");
-    let mut has_fn_info = false;
-    let mut fn_definition = MaybeUninit::uninit();
-    let handle = unsafe {
-        mun_runtime_get_function_definition(
-            driver.runtime,
-            fn_name.as_ptr(),
-            &mut has_fn_info as *mut _,
-            fn_definition.as_mut_ptr(),
-        )
-    };
-    assert_eq!(handle.token(), 0);
-
-    let fn_definition = unsafe { fn_definition.assume_init() };
-    // TODO: Simplify this once we have `mun_runtime_find_type_info`
-    let return_type = fn_definition.prototype.signature.return_type().unwrap();
-    let return_type =
-        UnsafeTypeInfo::new(NonNull::new(return_type as *const abi::TypeInfo as *mut _).unwrap());
-
-    let mut obj = MaybeUninit::uninit();
-    let handle = unsafe { mun_gc_alloc(driver.runtime, return_type, obj.as_mut_ptr()) };
-    assert_eq!(handle.token(), 0);
-
-    let obj = unsafe { obj.assume_init() };
-    assert_ne!(unsafe { obj.deref::<u8>() }, ptr::null());
-
-    let mut reclaimed = false;
-    let handle = unsafe { mun_gc_collect(driver.runtime, &mut reclaimed as *mut _) };
-    assert_eq!(handle.token(), 0);
-}
-
-#[test]
-fn test_gc_ptr_type_invalid_type_info() {
-    let driver = TestDriver::new(
-        r#"
-        pub struct Foo;
-
-        pub fn main() -> Foo { Foo }
-    "#,
-    );
-
-    let handle = unsafe {
-        let raw_ptr: RawGcPtr = ptr::null();
-        mun_gc_ptr_type(driver.runtime, raw_ptr.into(), ptr::null_mut())
-    };
-
-    let message = unsafe { CStr::from_ptr(mun_error_message(handle)) };
-    assert_eq!(
-        message.to_str().unwrap(),
-        "Invalid argument: 'type_info' is null pointer."
-    );
-
-    unsafe { mun_destroy_string(message.as_ptr()) };
-}
-
-#[test]
-fn test_gc_ptr_type() {
-    let driver = TestDriver::new(
-        r#"
-        pub struct Foo;
-
-        pub fn main() -> Foo { Foo }
-    "#,
-    );
-    let fn_name = CString::new("main").expect("Invalid function name");
-    let mut has_fn_info = false;
-    let mut fn_definition = MaybeUninit::uninit();
-    let handle = unsafe {
-        mun_runtime_get_function_definition(
-            driver.runtime,
-            fn_name.as_ptr(),
-            &mut has_fn_info as *mut _,
-            fn_definition.as_mut_ptr(),
-        )
-    };
-    assert_eq!(handle.token(), 0);
-
-    let fn_definition = unsafe { fn_definition.assume_init() };
-    // TODO: Simplify this once we have `mun_runtime_find_type_info`
-    let return_type = fn_definition.prototype.signature.return_type().unwrap();
-    let return_type =
-        UnsafeTypeInfo::new(NonNull::new(return_type as *const abi::TypeInfo as *mut _).unwrap());
-
-    let mut obj = MaybeUninit::uninit();
-    let handle = unsafe { mun_gc_alloc(driver.runtime, return_type, obj.as_mut_ptr()) };
-    assert_eq!(handle.token(), 0);
-
-    let obj = unsafe { obj.assume_init() };
-    assert_ne!(unsafe { obj.deref::<u8>() }, ptr::null());
-
-    let mut ty = MaybeUninit::uninit();
-    let handle = unsafe { mun_gc_ptr_type(driver.runtime, obj, ty.as_mut_ptr()) };
-    assert_eq!(handle.token(), 0);
-
-    let _ty = unsafe { ty.assume_init() };
-
-    let mut reclaimed = false;
-    let handle = unsafe { mun_gc_collect(driver.runtime, &mut reclaimed as *mut _) };
-    assert_eq!(handle.token(), 0);
-    assert!(reclaimed);
-}
-
-#[test]
-fn test_gc_rooting() {
-    let driver = TestDriver::new(
-        r#"
-        pub struct Foo;
-
-        pub fn main() -> Foo { Foo }
-    "#,
-    );
-    let fn_name = CString::new("main").expect("Invalid function name");
-    let mut has_fn_info = false;
-    let mut fn_definition = MaybeUninit::uninit();
-    let handle = unsafe {
-        mun_runtime_get_function_definition(
-            driver.runtime,
-            fn_name.as_ptr(),
-            &mut has_fn_info as *mut _,
-            fn_definition.as_mut_ptr(),
-        )
-    };
-    assert_eq!(handle.token(), 0);
-
-    let fn_definition = unsafe { fn_definition.assume_init() };
-    // TODO: Simplify this once we have `mun_runtime_find_type_info`
-    let return_type = fn_definition.prototype.signature.return_type().unwrap();
-    let return_type =
-        UnsafeTypeInfo::new(NonNull::new(return_type as *const abi::TypeInfo as *mut _).unwrap());
-
-    let mut obj = MaybeUninit::uninit();
-    let handle = unsafe { mun_gc_alloc(driver.runtime, return_type, obj.as_mut_ptr()) };
-    assert_eq!(handle.token(), 0);
-
-    let obj = unsafe { obj.assume_init() };
-    assert_ne!(unsafe { obj.deref::<u8>() }, ptr::null());
-
-    let handle = unsafe { mun_gc_root(driver.runtime, obj) };
-
-    assert_eq!(handle.token(), 0);
-
-    let mut reclaimed = false;
-    let handle = unsafe { mun_gc_collect(driver.runtime, &mut reclaimed as *mut _) };
-    assert_eq!(handle.token(), 0);
-    assert!(!reclaimed);
-
-    let handle = unsafe { mun_gc_unroot(driver.runtime, obj) };
-    assert_eq!(handle.token(), 0);
-
-    let handle = unsafe { mun_gc_collect(driver.runtime, &mut reclaimed as *mut _) };
-    assert_eq!(handle.token(), 0);
-    assert!(reclaimed);
-}
-
-#[test]
-fn test_gc_ptr_collect_invalid_reclaimed() {
-    let driver = TestDriver::new(
-        r#"
-        pub struct Foo;
-
-        pub fn main() -> Foo { Foo }
-    "#,
-    );
-
-    let handle = unsafe { mun_gc_collect(driver.runtime, ptr::null_mut()) };
-
-    let message = unsafe { CStr::from_ptr(mun_error_message(handle)) };
-    assert_eq!(
-        message.to_str().unwrap(),
-        "Invalid argument: 'reclaimed' is null pointer."
-    );
-
-    unsafe { mun_destroy_string(message.as_ptr()) };
-}