Repository: WebAssembly/binaryen
Pull request #7396: Initial support for exact heap types
Author: login: tlively
Created: 2025-03-25T03:48:13Z | Merged: 2025-03-27T00:19:59Z

The custom descriptors proposal has moved exactness from reference types
to defined (but not abstract) heap types. Since we only use a bit in the
heap type representation to represent sharedness for abstract heap
types, we can conveniently reuse the same bit to represent exactness for
heap types.

Implement basic support for representing exact heap types and taking
them into account in canonicalization. Also ensure that other operations
like getting the rec group of a heap type or looking up its structure
work properly on exact heap types.



Diff:
diff --git a/src/wasm-type.h b/src/wasm-type.h
index a72ba9d2cfa..47b08ef9cc6 100644
--- a/src/wasm-type.h
+++ b/src/wasm-type.h
@@ -62,6 +62,7 @@ using Tuple = TypeList;
 
 enum Nullability { NonNullable, Nullable };
 enum Mutability { Immutable, Mutable };
+enum Exactness { Inexact, Exact };
 
 // HeapType name information used for printing.
 struct TypeNames {
@@ -98,10 +99,12 @@ class HeapType {
   static constexpr int TypeBits = 2;
   static constexpr int UsedBits = TypeBits + 1;
   static constexpr int SharedMask = 1 << TypeBits;
+  static constexpr int ExactMask = SharedMask;
 
 public:
-  // Bits 0-1 are used by the Type representation, so need to be left free.
-  // Bit 2 determines whether the basic heap type is shared (1) or unshared (0).
+  // Bits 0-1 are used by the Type representation, so need to be left free. Bit
+  // 2 determines whether a basic heap type is shared (1) or unshared (0). For
+  // non-basic heap types, bit 2 determines whether the type is exact instead.
   enum BasicHeapType : uint32_t {
     ext = 1 << UsedBits,
     func = 2 << UsedBits,
@@ -126,7 +129,7 @@ class HeapType {
   constexpr HeapType(BasicHeapType id) : id(id) {}
 
   // But converting raw TypeID is more dangerous, so make it explicit
-  explicit HeapType(TypeID id) : id(id) {}
+  explicit constexpr HeapType(TypeID id) : id(id) {}
 
   // Choose an arbitrary heap type as the default.
   constexpr HeapType() : HeapType(func) {}
@@ -167,8 +170,12 @@ class HeapType {
   bool isBottom() const;
   bool isOpen() const;
   bool isShared() const { return getShared() == Shared; }
+  bool isExact() const { return getExactness() == Exact; }
 
   Shareability getShared() const;
+  Exactness getExactness() const {
+    return !isBasic() && (id & ExactMask) ? Exact : Inexact;
+  }
 
   // Check if the type is a given basic heap type, while ignoring whether it is
   // shared or not.
@@ -217,8 +224,6 @@ class HeapType {
   // Get the index of this non-basic type within its recursion group.
   size_t getRecGroupIndex() const;
 
-  constexpr TypeID getID() const { return id; }
-
   // Get the shared or unshared version of this basic heap type.
   constexpr BasicHeapType getBasic(Shareability share) const {
     assert(isBasic());
@@ -226,6 +231,22 @@ class HeapType {
                                          : (id & ~SharedMask));
   }
 
+  constexpr HeapType with(Exactness exactness) const {
+    assert((!isBasic() || exactness == Inexact) &&
+           "abstract types cannot be exact");
+    return HeapType(exactness == Exact ? (id | ExactMask) : (id & ~ExactMask));
+  }
+
+  // The ID is the numeric representation of the heap type and can be used in
+  // FFI or hashing applications. The "raw" ID is the numeric representation of
+  // the plain version of the type without exactness or any other attributes we
+  // might add in the future. It's useful in contexts where all heap types using
+  // the same type definition need to be treated identically.
+  constexpr TypeID getID() const { return id; }
+  constexpr TypeID getRawID() const {
+    return isBasic() ? id : with(Inexact).id;
+  }
+
   // (In)equality must be defined for both HeapType and BasicHeapType because it
   // is otherwise ambiguous whether to convert both this and other to int or
   // convert other to HeapType.
diff --git a/src/wasm/wasm-type.cpp b/src/wasm/wasm-type.cpp
index 5cdb76c19dd..8a36dee7472 100644
--- a/src/wasm/wasm-type.cpp
+++ b/src/wasm/wasm-type.cpp
@@ -228,7 +228,7 @@ namespace {
 
 HeapTypeInfo* getHeapTypeInfo(HeapType ht) {
   assert(!ht.isBasic());
-  return (HeapTypeInfo*)ht.getID();
+  return (HeapTypeInfo*)(ht.getRawID());
 }
 
 HeapType asHeapType(std::unique_ptr<HeapTypeInfo>& info) {
@@ -1247,7 +1247,7 @@ RecGroup HeapType::getRecGroup() const {
   } else {
     // Mark the low bit to signify that this is a trivial recursion group and
     // points to a heap type info rather than a vector of heap types.
-    return RecGroup(id | 1);
+    return RecGroup(getRawID() | 1);
   }
 }
 
@@ -1608,14 +1608,20 @@ bool SubTyper::isSubType(const Array& a, const Array& b) {
 }
 
 void TypePrinter::printHeapTypeName(HeapType type) {
+  if (type.isExact()) {
+    os << "(exact ";
+  }
   if (type.isBasic()) {
     print(type);
-    return;
-  }
-  generator(type).name.print(os);
+  } else {
+    generator(type).name.print(os);
 #if TRACE_CANONICALIZATION
-  os << "(;" << ((type.getID() >> 4) % 1000) << ";) ";
+    os << "(;" << ((type.getID() >> 4) % 1000) << ";) ";
 #endif
+  }
+  if (type.isExact()) {
+    os << ')';
+  }
 }
 
 std::ostream& TypePrinter::print(Type type) {
@@ -1942,8 +1948,10 @@ size_t RecGroupHasher::hash(HeapType type) const {
     wasm::rehash(digest, type.getID());
     return digest;
   }
+  wasm::rehash(digest, type.isExact());
   wasm::rehash(digest, type.getRecGroupIndex());
   auto currGroup = type.getRecGroup();
+  wasm::rehash(digest, currGroup != group);
   if (currGroup != group) {
     wasm::rehash(digest, currGroup.getID());
   }
@@ -2073,6 +2081,9 @@ bool RecGroupEquator::eq(HeapType a, HeapType b) const {
   if (a.isBasic() || b.isBasic()) {
     return a == b;
   }
+  if (a.getExactness() != b.getExactness()) {
+    return false;
+  }
   if (a.getRecGroupIndex() != b.getRecGroupIndex()) {
     return false;
   }
@@ -2456,8 +2467,10 @@ void updateReferencedHeapTypes(
       isTopLevel = false;
       if (type->isRef()) {
         auto ht = type->getHeapType();
+        auto exact = ht.getExactness();
+        ht = ht.with(Inexact);
         if (auto it = canonicalized.find(ht); it != canonicalized.end()) {
-          *type = Type(it->second, type->getNullability());
+          *type = Type(it->second.with(exact), type->getNullability());
         }
       } else if (type->isTuple()) {
         TypeGraphWalkerBase<ChildUpdater>::scanType(type);
@@ -2465,6 +2478,7 @@ void updateReferencedHeapTypes(
     }
 
     void scanHeapType(HeapType* type) {
+      assert(!type->isExact() && "unexpected exact type in definition");
       if (isTopLevel) {
         isTopLevel = false;
         TypeGraphWalkerBase<ChildUpdater>::scanHeapType(type);
@@ -2529,7 +2543,8 @@ buildRecGroup(std::unique_ptr<RecGroupInfo>&& groupInfo,
   for (size_t i = 0; i < typeInfos.size(); ++i) {
     auto type = asHeapType(typeInfos[i]);
     for (auto child : type.getHeapTypeChildren()) {
-      if (isTemp(child) && !seenTypes.count(child)) {
+      HeapType rawChild(child.getRawID());
+      if (isTemp(rawChild) && !seenTypes.count(rawChild)) {
         return {TypeBuilder::Error{
           i, TypeBuilder::ErrorReason::ForwardChildReference}};
       }
diff --git a/test/gtest/type-builder.cpp b/test/gtest/type-builder.cpp
index 1e676b7194a..305a0380ca3 100644
--- a/test/gtest/type-builder.cpp
+++ b/test/gtest/type-builder.cpp
@@ -428,6 +428,93 @@ TEST_F(TypeTest, CanonicalizeUses) {
   EXPECT_NE(built[4], built[6]);
 }
 
+TEST_F(TypeTest, CanonicalizeExactHeapTypes) {
+  TypeBuilder builder(8);
+
+  HeapType inexact = HeapType(builder[0]).with(Inexact);
+  HeapType exact = HeapType(builder[1]).with(Exact);
+
+  Type inexactRef = builder.getTempRefType(inexact, Nullable);
+  Type exactRef = builder.getTempRefType(exact, Nullable);
+
+  // Types that vary in exactness of the referenced heap type are different.
+  builder[0] = Struct({Field(inexactRef, Mutable)});
+  builder[1] = Struct({Field(exactRef, Mutable)});
+  builder[2] = Signature(Type({inexactRef, exactRef}), Type::none);
+  builder[3] = Signature(Type::none, Type({exactRef, inexactRef}));
+
+  auto translate = [&](HeapType t) {
+    for (int i = 0; i < 4; ++i) {
+      if (t.with(Inexact) == builder[i]) {
+        return HeapType(builder[4 + i]).with(t.getExactness());
+      }
+    }
+    WASM_UNREACHABLE("unexpected type");
+  };
+
+  builder[4].copy(builder[0], translate);
+  builder[5].copy(builder[1], translate);
+  builder[6].copy(builder[2], translate);
+  builder[7].copy(builder[3], translate);
+
+  auto result = builder.build();
+  ASSERT_TRUE(result);
+  auto built = *result;
+
+  // Different types should be different.
+  EXPECT_NE(built[0], built[1]);
+  EXPECT_NE(built[0], built[2]);
+  EXPECT_NE(built[0], built[3]);
+  EXPECT_NE(built[1], built[2]);
+  EXPECT_NE(built[1], built[3]);
+  EXPECT_NE(built[2], built[3]);
+
+  // Copies of the types should match.
+  EXPECT_EQ(built[0], built[4]);
+  EXPECT_EQ(built[1], built[5]);
+  EXPECT_EQ(built[2], built[6]);
+  EXPECT_EQ(built[3], built[7]);
+
+  // A type is inexact by default.
+  EXPECT_EQ(built[0], built[0].with(Inexact));
+  EXPECT_EQ(built[1], built[1].with(Inexact));
+  EXPECT_EQ(built[2], built[2].with(Inexact));
+  EXPECT_EQ(built[3], built[3].with(Inexact));
+
+  // We can freely convert between exact and inexact.
+  EXPECT_EQ(built[0], built[0].with(Exact).with(Inexact));
+  EXPECT_EQ(built[0].with(Exact),
+            built[0].with(Exact).with(Inexact).with(Exact));
+
+  // Conversions are idempotent.
+  EXPECT_EQ(built[0].with(Exact), built[0].with(Exact).with(Exact));
+  EXPECT_EQ(built[0], built[0].with(Inexact));
+
+  // An exact version of a type is not the same as its inexact version.
+  EXPECT_NE(built[0].with(Exact), built[0].with(Inexact));
+
+  // But they have the same rec group.
+  EXPECT_EQ(built[0].with(Exact).getRecGroup(),
+            built[0].with(Inexact).getRecGroup());
+
+  // Looking up the inner structure works either way.
+  ASSERT_TRUE(built[0].with(Exact).isStruct());
+  ASSERT_TRUE(built[0].with(Inexact).isStruct());
+  EXPECT_EQ(built[0].with(Exact).getStruct(),
+            built[0].with(Inexact).getStruct());
+
+  // The exactness of children types is preserved.
+  EXPECT_EQ(built[0], built[0].getStruct().fields[0].type.getHeapType());
+  EXPECT_EQ(built[1].with(Exact),
+            built[1].getStruct().fields[0].type.getHeapType());
+  EXPECT_EQ(built[0], built[2].getSignature().params[0].getHeapType());
+  EXPECT_EQ(built[1].with(Exact),
+            built[2].getSignature().params[1].getHeapType());
+  EXPECT_EQ(built[0], built[3].getSignature().results[1].getHeapType());
+  EXPECT_EQ(built[1].with(Exact),
+            built[3].getSignature().results[0].getHeapType());
+}
+
 TEST_F(TypeTest, CanonicalizeSelfReferences) {
   TypeBuilder builder(5);
   // Single self-reference


Reviews:
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T16:59:38Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T17:14:58Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T17:15:21Z
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T17:20:55Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T17:29:25Z
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T18:02:09Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T18:14:58Z
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T19:05:11Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T20:46:04Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T20:49:26Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T20:52:52Z
user: login: kripken
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-26T21:11:37Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T22:35:46Z
user: login: tlively
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T22:36:49Z
user: login: kripken
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T23:44:15Z

Review comments:
diff_hunk: @@ -99,10 +99,12 @@ class HeapType {
   static constexpr int TypeBits = 3;
   static constexpr int UsedBits = TypeBits + 1;
   static constexpr int SharedMask = 1 << TypeBits;
+  static constexpr int ExactMask = SharedMask;
 
 public:
-  // Bits 0-2 are used by the Type representation, so need to be left free.
-  // Bit 3 determines whether the basic heap type is shared (1) or unshared (0).
+  // Bits 0-1 are used by the Type representation, so need to be left free.
+  // Bit 2 determines whether basic heap type is shared (1) or unshared (0).
path: src/wasm-type.h
user: login: kripken
body: ```suggestion
  // Bit 2 determines whether a basic heap type is shared (1) or unshared (0).
```
created_at: 2025-03-25T16:57:18Z
author_association: MEMBER
diff_hunk: @@ -99,10 +99,12 @@ class HeapType {
   static constexpr int TypeBits = 3;
   static constexpr int UsedBits = TypeBits + 1;
   static constexpr int SharedMask = 1 << TypeBits;
+  static constexpr int ExactMask = SharedMask;
 
 public:
-  // Bits 0-2 are used by the Type representation, so need to be left free.
-  // Bit 3 determines whether the basic heap type is shared (1) or unshared (0).
+  // Bits 0-1 are used by the Type representation, so need to be left free.
+  // Bit 2 determines whether basic heap type is shared (1) or unshared (0).
+  // For non-basic heap types, bit 2 determines whether the type is exact.
path: src/wasm-type.h
user: login: kripken
body: In the current spec, it sounds like we disallow `(ref exact any)` or any other basic heap type?
created_at: 2025-03-25T16:58:47Z
author_association: MEMBER
diff_hunk: @@ -228,7 +228,7 @@ namespace {
 
 HeapTypeInfo* getHeapTypeInfo(HeapType ht) {
   assert(!ht.isBasic());
-  return (HeapTypeInfo*)ht.getID();
+  return (HeapTypeInfo*)(ht.with(Inexact).getID());
path: src/wasm/wasm-type.cpp
user: login: kripken
body: Why is this correct?
created_at: 2025-03-25T16:59:36Z
author_association: MEMBER
diff_hunk: @@ -228,7 +228,7 @@ namespace {
 
 HeapTypeInfo* getHeapTypeInfo(HeapType ht) {
   assert(!ht.isBasic());
-  return (HeapTypeInfo*)ht.getID();
+  return (HeapTypeInfo*)(ht.with(Inexact).getID());
path: src/wasm/wasm-type.cpp
user: login: tlively
body: This clears the exact bit in the ID, recovering the value of the pointer to the underlying HeapTypeInfo.
created_at: 2025-03-25T17:14:58Z
author_association: MEMBER
diff_hunk: @@ -99,10 +99,12 @@ class HeapType {
   static constexpr int TypeBits = 3;
   static constexpr int UsedBits = TypeBits + 1;
   static constexpr int SharedMask = 1 << TypeBits;
+  static constexpr int ExactMask = SharedMask;
 
 public:
-  // Bits 0-2 are used by the Type representation, so need to be left free.
-  // Bit 3 determines whether the basic heap type is shared (1) or unshared (0).
+  // Bits 0-1 are used by the Type representation, so need to be left free.
+  // Bit 2 determines whether basic heap type is shared (1) or unshared (0).
+  // For non-basic heap types, bit 2 determines whether the type is exact.
path: src/wasm-type.h
user: login: tlively
body: Yes, exactly.
created_at: 2025-03-25T17:15:21Z
author_association: MEMBER
diff_hunk: @@ -228,7 +228,7 @@ namespace {
 
 HeapTypeInfo* getHeapTypeInfo(HeapType ht) {
   assert(!ht.isBasic());
-  return (HeapTypeInfo*)ht.getID();
+  return (HeapTypeInfo*)(ht.with(Inexact).getID());
path: src/wasm/wasm-type.cpp
user: login: kripken
body: Then I'm surprised we don't need to clear out the other 2 reserved bits? In what way is bit 2 different than 0-1?
created_at: 2025-03-25T17:20:55Z
author_association: MEMBER
diff_hunk: @@ -228,7 +228,7 @@ namespace {
 
 HeapTypeInfo* getHeapTypeInfo(HeapType ht) {
   assert(!ht.isBasic());
-  return (HeapTypeInfo*)ht.getID();
+  return (HeapTypeInfo*)(ht.with(Inexact).getID());
path: src/wasm/wasm-type.cpp
user: login: tlively
body: Bits 0-1 are used in the `Type` representation and the `RecGroup` representation, but are always zero for `HeapType`.
created_at: 2025-03-25T17:29:25Z
author_association: MEMBER
diff_hunk: @@ -228,7 +228,7 @@ namespace {
 
 HeapTypeInfo* getHeapTypeInfo(HeapType ht) {
   assert(!ht.isBasic());
-  return (HeapTypeInfo*)ht.getID();
+  return (HeapTypeInfo*)(ht.with(Inexact).getID());
path: src/wasm/wasm-type.cpp
user: login: kripken
body: I see, thanks.

Perhaps we can add a helper for zeroing out the relevant bits?  That would be clearer than `with(Inexact)` and it would also be much easier to update in the future if we add more special bits.
created_at: 2025-03-25T18:02:09Z
author_association: MEMBER
diff_hunk: @@ -228,7 +228,7 @@ namespace {
 
 HeapTypeInfo* getHeapTypeInfo(HeapType ht) {
   assert(!ht.isBasic());
-  return (HeapTypeInfo*)ht.getID();
+  return (HeapTypeInfo*)(ht.with(Inexact).getID());
path: src/wasm/wasm-type.cpp
user: login: tlively
body: See the [rawID](https://github.com/WebAssembly/binaryen/pull/7396/commits/dcf0a1758c03525c6b4ad3ff6806abdaa621d27b) commit for this.
created_at: 2025-03-26T18:14:58Z
author_association: MEMBER
diff_hunk: @@ -217,15 +224,24 @@ class HeapType {
   // Get the index of this non-basic type within its recursion group.
   size_t getRecGroupIndex() const;
 
-  constexpr TypeID getID() const { return id; }
-
   // Get the shared or unshared version of this basic heap type.
   constexpr BasicHeapType getBasic(Shareability share) const {
     assert(isBasic());
     return BasicHeapType(share == Shared ? (id | SharedMask)
                                          : (id & ~SharedMask));
   }
 
+  constexpr HeapType with(Exactness exactness) const {
+    assert((!isBasic() || exactness == Inexact) &&
+           "abstract types cannot be exact");
+    return HeapType(exactness == Exact ? (id | ExactMask) : (id & ~ExactMask));
+  }
+
+  constexpr TypeID getID() const { return id; }
+  constexpr TypeID getRawID() const {
path: src/wasm-type.h
user: login: kripken
body: Please add a comment explaining the difference.

And, in particular - when would `getID()` still be used?
created_at: 2025-03-26T19:02:04Z
author_association: MEMBER
diff_hunk: @@ -2456,15 +2467,18 @@ void updateReferencedHeapTypes(
       isTopLevel = false;
       if (type->isRef()) {
         auto ht = type->getHeapType();
+        auto exact = ht.getExactness();
+        ht = ht.with(Inexact);
         if (auto it = canonicalized.find(ht); it != canonicalized.end()) {
-          *type = Type(it->second, type->getNullability());
+          *type = Type(it->second.with(exact), type->getNullability());
path: src/wasm/wasm-type.cpp
user: login: kripken
body: But if exactness is part of the heap type, why do we want this behavior?
created_at: 2025-03-26T19:05:02Z
author_association: MEMBER
diff_hunk: @@ -217,15 +224,24 @@ class HeapType {
   // Get the index of this non-basic type within its recursion group.
   size_t getRecGroupIndex() const;
 
-  constexpr TypeID getID() const { return id; }
-
   // Get the shared or unshared version of this basic heap type.
   constexpr BasicHeapType getBasic(Shareability share) const {
     assert(isBasic());
     return BasicHeapType(share == Shared ? (id | SharedMask)
                                          : (id & ~SharedMask));
   }
 
+  constexpr HeapType with(Exactness exactness) const {
+    assert((!isBasic() || exactness == Inexact) &&
+           "abstract types cannot be exact");
+    return HeapType(exactness == Exact ? (id | ExactMask) : (id & ~ExactMask));
+  }
+
+  constexpr TypeID getID() const { return id; }
+  constexpr TypeID getRawID() const {
path: src/wasm-type.h
user: login: tlively
body: For example when hashing a type or in the C API to convert the type to an integer.
created_at: 2025-03-26T20:46:04Z
author_association: MEMBER
diff_hunk: @@ -2456,15 +2467,18 @@ void updateReferencedHeapTypes(
       isTopLevel = false;
       if (type->isRef()) {
         auto ht = type->getHeapType();
+        auto exact = ht.getExactness();
+        ht = ht.with(Inexact);
         if (auto it = canonicalized.find(ht); it != canonicalized.end()) {
-          *type = Type(it->second, type->getNullability());
+          *type = Type(it->second.with(exact), type->getNullability());
path: src/wasm/wasm-type.cpp
user: login: tlively
body: This is replacing non-canonical heap types with canonical heap types. If `$t'` is the canonical version of `$t`, we need to update uses of `(exact $t)` to be `(exact $t')` instead. The exact type will not appear in the `canonicalized` map because we are canonicalizing type definitions represented by the inexact defined types. `(exact $t)` and `$t` refer to the same type definition, so it would be redundant to store the exact version in the map.
created_at: 2025-03-26T20:49:26Z
author_association: MEMBER
diff_hunk: @@ -217,15 +224,24 @@ class HeapType {
   // Get the index of this non-basic type within its recursion group.
   size_t getRecGroupIndex() const;
 
-  constexpr TypeID getID() const { return id; }
-
   // Get the shared or unshared version of this basic heap type.
   constexpr BasicHeapType getBasic(Shareability share) const {
     assert(isBasic());
     return BasicHeapType(share == Shared ? (id | SharedMask)
                                          : (id & ~SharedMask));
   }
 
+  constexpr HeapType with(Exactness exactness) const {
+    assert((!isBasic() || exactness == Inexact) &&
+           "abstract types cannot be exact");
+    return HeapType(exactness == Exact ? (id | ExactMask) : (id & ~ExactMask));
+  }
+
+  constexpr TypeID getID() const { return id; }
+  constexpr TypeID getRawID() const {
path: src/wasm-type.h
user: login: tlively
body: Added the comment.
created_at: 2025-03-26T20:52:52Z
author_association: MEMBER
diff_hunk: @@ -228,7 +228,7 @@ namespace {
 
 HeapTypeInfo* getHeapTypeInfo(HeapType ht) {
   assert(!ht.isBasic());
-  return (HeapTypeInfo*)ht.getID();
+  return (HeapTypeInfo*)(ht.getRawID());
path: src/wasm/wasm-type.cpp
user: login: kripken
body: I don't feel strongly but `getRawID` could maybe be `getPointer`.
created_at: 2025-03-26T21:01:33Z
author_association: MEMBER
diff_hunk: @@ -2456,15 +2467,18 @@ void updateReferencedHeapTypes(
       isTopLevel = false;
       if (type->isRef()) {
         auto ht = type->getHeapType();
+        auto exact = ht.getExactness();
+        ht = ht.with(Inexact);
         if (auto it = canonicalized.find(ht); it != canonicalized.end()) {
-          *type = Type(it->second, type->getNullability());
+          *type = Type(it->second.with(exact), type->getNullability());
path: src/wasm/wasm-type.cpp
user: login: kripken
body: I see, thanks. Why does Shared not need to be handled similarly here? Because for non-basic types we store it on HeapTypeInfo?
created_at: 2025-03-26T21:07:35Z
author_association: MEMBER
diff_hunk: @@ -2456,15 +2467,18 @@ void updateReferencedHeapTypes(
       isTopLevel = false;
       if (type->isRef()) {
         auto ht = type->getHeapType();
+        auto exact = ht.getExactness();
+        ht = ht.with(Inexact);
         if (auto it = canonicalized.find(ht); it != canonicalized.end()) {
-          *type = Type(it->second, type->getNullability());
+          *type = Type(it->second.with(exact), type->getNullability());
path: src/wasm/wasm-type.cpp
user: login: tlively
body: Right. Sharedness is actually part of the type definition, unlike exactness.
created_at: 2025-03-26T22:35:45Z
author_association: MEMBER
diff_hunk: @@ -228,7 +228,7 @@ namespace {
 
 HeapTypeInfo* getHeapTypeInfo(HeapType ht) {
   assert(!ht.isBasic());
-  return (HeapTypeInfo*)ht.getID();
+  return (HeapTypeInfo*)(ht.getRawID());
path: src/wasm/wasm-type.cpp
user: login: tlively
body: What do you think about `getDefinitionID` or `getBaseID`?
created_at: 2025-03-26T22:36:48Z
author_association: MEMBER
diff_hunk: @@ -228,7 +228,7 @@ namespace {
 
 HeapTypeInfo* getHeapTypeInfo(HeapType ht) {
   assert(!ht.isBasic());
-  return (HeapTypeInfo*)ht.getID();
+  return (HeapTypeInfo*)(ht.getRawID());
path: src/wasm/wasm-type.cpp
user: login: kripken
body: Hmm, no real preference between those. Current name is fine.
created_at: 2025-03-26T23:44:15Z
author_association: MEMBER

PR comments:
user: login: kripken
created_at: 2025-03-25T17:00:31Z
body: It is convenient to reuse the same bit, but still, maybe it makes sense to back out the old exactness? I just imagine that if I run into some exactness-related bug, debugging it will be much harder if the issue could be one of two exactness representations.
author_association: MEMBER
user: login: tlively
created_at: 2025-03-25T17:16:40Z
body: Yes, I will definitely back out the exactness on reference types, but I wanted to wait until we had a chance to validate that there are no huge issues with putting the exactness on the heap types instead.
author_association: MEMBER
user: login: tlively
created_at: 2025-03-26T01:51:35Z
body: The problem with the Emscripten tests is that the 8-byte alignment provided by Emscripten's allocator means that the addresses of allocated HeapTypeInfos might use bit 3 and conflict with our use of it to represent exactness. To fix this problem I could either increase the alignment of all `HeapTypeInfo` allocations or I could back out exact reference types to allow us to use bit 2 to represent heap type exactness. On the one hand, increasing the allocation alignment would be wasted work since we plan to back out exact reference types anyway, but on the other hand we may need to increase the allocation alignment anyway if we need to use more bits in the future.
author_association: MEMBER