spirv parser: create ast types along with sem types

The spirv parser now creates ast types along with sem types via
typ::Type. All sem::Type* were replaced with typ::Type, and its `ast`
member is used over the `sem` member to make it easier to migrate to
ast-only.

The parser was written to take advantage of the fact that types were
resolved to semantic types during parsing. For instance, a mapping of
spirv typeid to sem::Type* was used throughout (`id_to_type_`) to
resolve types once, and to support type aliasing. Since the goal is to
only create AST types, and to resolve only in the Resolver, I made many
changes to remove this dependency on semantic types. For instance, we
now always call ConvertType(typeid) instead of looking up via
id_to_type. Similarly, the `signed_type_for_` and `unsigned_type_for_`
maps were replaced with `UnsignedTypeFor` and `SignedTypeFor` functions.

Bug: tint:724
Change-Id: I3aee3928834febd71b473d6a8d8cb77b1ac94e21
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/49542
Reviewed-by: David Neto <dneto@google.com>
Commit-Queue: Antonio Maiorano <amaiorano@google.com>
diff --git a/src/ast/type.h b/src/ast/type.h
index cd0bc0f..c62d59b 100644
--- a/src/ast/type.h
+++ b/src/ast/type.h
@@ -48,6 +48,12 @@
 
   /// @returns the most deeply nested aliased type if this is an alias, `this`
   /// otherwise
+  const Type* UnwrapAliasIfNeeded() const {
+    return const_cast<Type*>(this)->UnwrapAliasIfNeeded();
+  }
+
+  /// @returns the most deeply nested aliased type if this is an alias, `this`
+  /// otherwise
   Type* UnwrapAliasIfNeeded();
 
   /// Removes all levels of aliasing and access control.
@@ -58,6 +64,16 @@
   /// @returns the completely unaliased type.
   Type* UnwrapIfNeeded();
 
+  /// Removes all levels of aliasing and access control.
+  /// This is just enough to assist with WGSL translation
+  /// in that you want see through one level of pointer to get from an
+  /// identifier-like expression as an l-value to its corresponding r-value,
+  /// plus see through the wrappers on either side.
+  /// @returns the completely unaliased type.
+  const Type* UnwrapIfNeeded() const {
+    return const_cast<Type*>(this)->UnwrapIfNeeded();
+  }
+
   /// Returns the type found after:
   /// - removing all layers of aliasing and access control if they exist, then
   /// - removing the pointer, if it exists, then
@@ -65,6 +81,13 @@
   /// @returns the unwrapped type
   Type* UnwrapAll();
 
+  /// Returns the type found after:
+  /// - removing all layers of aliasing and access control if they exist, then
+  /// - removing the pointer, if it exists, then
+  /// - removing all further layers of aliasing or access control, if they exist
+  /// @returns the unwrapped type
+  const Type* UnwrapAll() const { return const_cast<Type*>(this)->UnwrapAll(); }
+
   /// @returns true if this type is a scalar
   bool is_scalar() const;
   /// @returns true if this type is a float scalar
diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc
index 5e206bb..5b17ca5 100644
--- a/src/reader/spirv/function.cc
+++ b/src/reader/spirv/function.cc
@@ -690,6 +690,13 @@
   ast::BlockStatement* continuing = nullptr;
 };
 
+// Forwards UnwrapAll to both the ast and sem types of the TypePair
+// @param tp the type pair
+// @returns the unwrapped type pair
+typ::Type UnwrapAll(typ::Type tp) {
+  return typ::Type{tp.ast->UnwrapAll(), tp.sem->UnwrapAll()};
+}
+
 }  // namespace
 
 BlockInfo::BlockInfo(const spvtools::opt::BasicBlock& bb)
@@ -726,8 +733,6 @@
       fail_stream_(pi->fail_stream()),
       namer_(pi->namer()),
       function_(function),
-      i32_(builder_.create<sem::I32>()),
-      u32_(builder_.create<sem::U32>()),
       sample_mask_in_id(0u),
       sample_mask_out_id(0u),
       ep_info_(ep_info) {
@@ -890,7 +895,7 @@
   // Surprisingly, the "type id" on an OpFunction is the result type of the
   // function, not the type of the function.  This is the one exceptional case
   // in SPIR-V where the type ID is not the type of the result ID.
-  auto* ret_ty = parser_impl_.ConvertType(function_.type_id());
+  auto ret_ty = parser_impl_.ConvertType(function_.type_id());
   if (failed()) {
     return false;
   }
@@ -903,7 +908,7 @@
   ast::VariableList ast_params;
   function_.ForEachParam(
       [this, &ast_params](const spvtools::opt::Instruction* param) {
-        auto* ast_type = parser_impl_.ConvertType(param->type_id());
+        auto ast_type = parser_impl_.ConvertType(param->type_id());
         if (ast_type != nullptr) {
           auto* ast_param = parser_impl_.MakeVariable(
               param->result_id(), ast::StorageClass::kNone, ast_type, true,
@@ -933,7 +938,7 @@
   return success();
 }
 
-sem::Type* FunctionEmitter::GetVariableStoreType(
+typ::Type FunctionEmitter::GetVariableStoreType(
     const spvtools::opt::Instruction& var_decl_inst) {
   const auto type_id = var_decl_inst.type_id();
   auto* var_ref_type = type_mgr_->GetType(type_id);
@@ -1996,7 +2001,7 @@
     if (inst.opcode() != SpvOpVariable) {
       continue;
     }
-    auto* var_store_type = GetVariableStoreType(inst);
+    auto var_store_type = GetVariableStoreType(inst);
     if (failed()) {
       return false;
     }
@@ -2062,7 +2067,7 @@
     case SkipReason::kSampleMaskOutBuiltinPointer:
       // The result type is always u32.
       auto name = namer_.Name(sample_mask_out_id);
-      return TypedExpression{u32_,
+      return TypedExpression{builder_.ty.u32(),
                              create<ast::IdentifierExpression>(
                                  Source{}, builder_.Symbols().Register(name))};
   }
@@ -2342,7 +2347,7 @@
         Source{},                                 // source
         builder_.Symbols().Register(guard_name),  // symbol
         ast::StorageClass::kFunction,             // storage_class
-        parser_impl_.Bool(),                      // type
+        builder_.ty.bool_(),                      // type
         false,                                    // is_const
         MakeTrue(Source{}),                       // constructor
         ast::DecorationList{});                   // decorations
@@ -2643,7 +2648,7 @@
         if (result_type->AsVoid() != nullptr) {
           AddStatement(create<ast::ReturnStatement>(Source{}));
         } else {
-          auto* ast_type = parser_impl_.ConvertType(function_.type_id());
+          auto ast_type = parser_impl_.ConvertType(function_.type_id());
           AddStatement(create<ast::ReturnStatement>(
               Source{}, parser_impl_.MakeNullValue(ast_type)));
         }
@@ -2888,7 +2893,7 @@
   for (auto id : sorted_by_index(block_info.hoisted_ids)) {
     const auto* def_inst = def_use_mgr_->GetDef(id);
     TINT_ASSERT(def_inst);
-    auto* ast_type =
+    auto ast_type =
         RemapStorageClass(parser_impl_.ConvertType(def_inst->type_id()), id);
     AddStatement(create<ast::VariableDeclStatement>(
         Source{},
@@ -3092,11 +3097,12 @@
 
         case SkipReason::kSampleMaskOutBuiltinPointer:
           ptr_id = sample_mask_out_id;
-          if (rhs.type != u32_) {
+          if (rhs.type != builder_.ty.u32()) {
             // WGSL requires sample_mask_out to be signed.
-            rhs = TypedExpression{
-                u32_, create<ast::TypeConstructorExpression>(
-                          Source{}, u32_, ast::ExpressionList{rhs.expr})};
+            rhs = TypedExpression{builder_.ty.u32(),
+                                  create<ast::TypeConstructorExpression>(
+                                      Source{}, builder_.ty.u32(),
+                                      ast::ExpressionList{rhs.expr})};
           }
           break;
         default:
@@ -3137,20 +3143,21 @@
           ast::Expression* id_expr = create<ast::IdentifierExpression>(
               Source{}, builder_.Symbols().Register(name));
           auto expr = TypedExpression{
-              i32_, create<ast::TypeConstructorExpression>(
-                        Source{}, i32_, ast::ExpressionList{id_expr})};
+              builder_.ty.i32(),
+              create<ast::TypeConstructorExpression>(
+                  Source{}, builder_.ty.i32(), ast::ExpressionList{id_expr})};
           return EmitConstDefinition(inst, expr);
         }
         case SkipReason::kSampleMaskInBuiltinPointer: {
           auto name = namer_.Name(sample_mask_in_id);
           ast::Expression* id_expr = create<ast::IdentifierExpression>(
               Source{}, builder_.Symbols().Register(name));
-          auto* load_result_type = parser_impl_.ConvertType(inst.type_id());
+          auto load_result_type = parser_impl_.ConvertType(inst.type_id());
           ast::Expression* ast_expr = nullptr;
-          if (load_result_type == i32_) {
+          if (load_result_type == builder_.ty.i32()) {
             ast_expr = create<ast::TypeConstructorExpression>(
-                Source{}, i32_, ast::ExpressionList{id_expr});
-          } else if (load_result_type == u32_) {
+                Source{}, builder_.ty.i32(), ast::ExpressionList{id_expr});
+          } else if (load_result_type == builder_.ty.u32()) {
             ast_expr = id_expr;
           } else {
             return Fail() << "loading the whole SampleMask input array is not "
@@ -3169,8 +3176,8 @@
       }
 
       // The load result type is the pointee type of its operand.
-      TINT_ASSERT(expr.type->Is<sem::Pointer>());
-      expr.type = expr.type->As<sem::Pointer>()->type();
+      TINT_ASSERT(expr.type.ast->Is<ast::Pointer>());
+      expr.type = typ::Call_type(typ::As<typ::Pointer>(expr.type));
       return EmitConstDefOrWriteToHoistedVar(inst, expr);
     }
 
@@ -3272,7 +3279,7 @@
 
   const auto opcode = inst.opcode();
 
-  sem::Type* ast_type =
+  typ::Type ast_type =
       inst.type_id() != 0 ? parser_impl_.ConvertType(inst.type_id()) : nullptr;
 
   auto binary_op = ConvertBinaryOp(opcode);
@@ -3416,7 +3423,7 @@
   auto* func = create<ast::IdentifierExpression>(
       Source{}, builder_.Symbols().Register(name));
   ast::ExpressionList operands;
-  sem::Type* first_operand_type = nullptr;
+  typ::Type first_operand_type = nullptr;
   // All parameters to GLSL.std.450 extended instructions are IDs.
   for (uint32_t iarg = 2; iarg < inst.NumInOperands(); ++iarg) {
     TypedExpression operand = MakeOperand(inst, iarg);
@@ -3425,7 +3432,7 @@
     }
     operands.emplace_back(operand.expr);
   }
-  auto* ast_type = parser_impl_.ConvertType(inst.type_id());
+  auto ast_type = parser_impl_.ConvertType(inst.type_id());
   auto* call = create<ast::CallExpression>(Source{}, func, std::move(operands));
   TypedExpression call_expr{ast_type, call};
   return parser_impl_.RectifyForcedResultType(call_expr, inst,
@@ -3660,9 +3667,9 @@
     }
     const auto pointer_type_id =
         type_mgr_->FindPointerToType(pointee_type_id, storage_class);
-    auto* ast_pointer_type = parser_impl_.ConvertType(pointer_type_id);
+    auto ast_pointer_type = parser_impl_.ConvertType(pointer_type_id);
     TINT_ASSERT(ast_pointer_type);
-    TINT_ASSERT(ast_pointer_type->Is<sem::Pointer>());
+    TINT_ASSERT(ast_pointer_type.ast->Is<ast::Pointer>());
     current_expr = TypedExpression{ast_pointer_type, next_expr};
   }
   return current_expr;
@@ -3826,7 +3833,6 @@
 }
 
 ast::Expression* FunctionEmitter::MakeFalse(const Source& source) const {
-  sem::Bool bool_type;
   return create<ast::ScalarConstructorExpression>(
       source, create<ast::BoolLiteral>(source, false));
 }
@@ -3847,8 +3853,8 @@
   // Generate an ast::TypeConstructor expression.
   // Assume the literal indices are valid, and there is a valid number of them.
   auto source = GetSourceForInst(inst);
-  sem::Vector* result_type =
-      parser_impl_.ConvertType(inst.type_id())->As<sem::Vector>();
+  typ::Vector result_type =
+      typ::As<typ::Vector>(parser_impl_.ConvertType(inst.type_id()));
   ast::ExpressionList values;
   for (uint32_t i = 2; i < inst.NumInOperands(); ++i) {
     const auto index = inst.GetSingleWordInOperand(i);
@@ -3870,7 +3876,8 @@
           source, expr.expr, Swizzle(sub_index)));
     } else if (index == 0xFFFFFFFF) {
       // By rule, this maps to OpUndef.  Instead, make it zero.
-      values.emplace_back(parser_impl_.MakeNullValue(result_type->type()));
+      values.emplace_back(
+          parser_impl_.MakeNullValue(typ::Call_type(result_type)));
     } else {
       Fail() << "invalid vectorshuffle ID %" << inst.result_id()
              << ": index too large: " << index;
@@ -3946,8 +3953,8 @@
       const auto* type = type_mgr_->GetType(inst.type_id());
       if (type) {
         if (type->AsPointer()) {
-          if (const auto* ast_type = parser_impl_.ConvertType(inst.type_id())) {
-            if (auto* ptr = ast_type->As<sem::Pointer>()) {
+          if (auto ast_type = parser_impl_.ConvertType(inst.type_id())) {
+            if (auto* ptr = ast_type.ast->As<ast::Pointer>()) {
               info->storage_class = ptr->storage_class();
             }
           }
@@ -3991,22 +3998,22 @@
   }
   const auto type_id = def_use_mgr_->GetDef(id)->type_id();
   if (type_id) {
-    auto* ast_type = parser_impl_.ConvertType(type_id);
-    if (ast_type && ast_type->Is<sem::Pointer>()) {
-      return ast_type->As<sem::Pointer>()->storage_class();
+    auto ast_type = parser_impl_.ConvertType(type_id);
+    if (auto ptr = typ::As<typ::Pointer>(ast_type)) {
+      return ptr.ast->storage_class();
     }
   }
   return ast::StorageClass::kNone;
 }
 
-sem::Type* FunctionEmitter::RemapStorageClass(sem::Type* type,
-                                              uint32_t result_id) {
-  if (const auto* ast_ptr_type = type->As<sem::Pointer>()) {
+typ::Type FunctionEmitter::RemapStorageClass(typ::Type type,
+                                             uint32_t result_id) {
+  if (auto ast_ptr_type = typ::As<typ::Pointer>(type)) {
     // Remap an old-style storage buffer pointer to a new-style storage
     // buffer pointer.
     const auto sc = GetStorageClassForPointerValue(result_id);
-    if (ast_ptr_type->storage_class() != sc) {
-      return builder_.create<sem::Pointer>(ast_ptr_type->type(), sc);
+    if (ast_ptr_type.ast->storage_class() != sc) {
+      return builder_.ty.pointer(typ::Call_type(ast_ptr_type), sc);
     }
   }
   return type;
@@ -4183,13 +4190,13 @@
 TypedExpression FunctionEmitter::MakeNumericConversion(
     const spvtools::opt::Instruction& inst) {
   const auto opcode = inst.opcode();
-  auto* requested_type = parser_impl_.ConvertType(inst.type_id());
+  auto requested_type = parser_impl_.ConvertType(inst.type_id());
   auto arg_expr = MakeOperand(inst, 0);
   if (!arg_expr.expr || !arg_expr.type) {
     return {};
   }
 
-  sem::Type* expr_type = nullptr;
+  typ::Type expr_type = nullptr;
   if ((opcode == SpvOpConvertSToF) || (opcode == SpvOpConvertUToF)) {
     if (arg_expr.type->is_integer_scalar_or_vector()) {
       expr_type = requested_type;
@@ -4248,13 +4255,13 @@
   }
   auto* call_expr =
       create<ast::CallExpression>(Source{}, function, std::move(params));
-  auto* result_type = parser_impl_.ConvertType(inst.type_id());
+  auto result_type = parser_impl_.ConvertType(inst.type_id());
   if (!result_type) {
     return Fail() << "internal error: no mapped type result of call: "
                   << inst.PrettyPrint();
   }
 
-  if (result_type->Is<sem::Void>()) {
+  if (result_type.ast->Is<ast::Void>()) {
     return nullptr !=
            AddStatement(create<ast::CallStatement>(Source{}, call_expr));
   }
@@ -4317,7 +4324,7 @@
       Source{}, builder_.Symbols().Register(name));
 
   ast::ExpressionList params;
-  sem::Type* first_operand_type = nullptr;
+  typ::Type first_operand_type = nullptr;
   for (uint32_t iarg = 0; iarg < inst.NumInOperands(); ++iarg) {
     TypedExpression operand = MakeOperand(inst, iarg);
     if (first_operand_type == nullptr) {
@@ -4327,7 +4334,7 @@
   }
   auto* call_expr =
       create<ast::CallExpression>(Source{}, ident, std::move(params));
-  auto* result_type = parser_impl_.ConvertType(inst.type_id());
+  auto result_type = parser_impl_.ConvertType(inst.type_id());
   if (!result_type) {
     Fail() << "internal error: no mapped type result of call: "
            << inst.PrettyPrint();
@@ -4348,9 +4355,9 @@
   // - operand1, operand2, and result type to match.
   // - you can't select over pointers or pointer vectors, unless you also have
   //   a VariablePointers* capability, which is not allowed in by WebGPU.
-  auto* op_ty = operand1.type;
-  if (op_ty->Is<sem::Vector>() || op_ty->is_float_scalar() ||
-      op_ty->is_integer_scalar() || op_ty->Is<sem::Bool>()) {
+  auto* op_ty = operand1.type.ast;
+  if (op_ty->Is<ast::Vector>() || op_ty->is_float_scalar() ||
+      op_ty->is_integer_scalar() || op_ty->Is<ast::Bool>()) {
     ast::ExpressionList params;
     params.push_back(operand1.expr);
     params.push_back(operand2.expr);
@@ -4388,18 +4395,23 @@
   return image;
 }
 
-sem::Texture* FunctionEmitter::GetImageType(
+typ::Texture FunctionEmitter::GetImageType(
     const spvtools::opt::Instruction& image) {
-  sem::Pointer* ptr_type = parser_impl_.GetTypeForHandleVar(image);
+  typ::Pointer ptr_type = parser_impl_.GetTypeForHandleVar(image);
   if (!parser_impl_.success()) {
     Fail();
-    return nullptr;
+    return {};
   }
-  if (!ptr_type || !ptr_type->type()->UnwrapAll()->Is<sem::Texture>()) {
+  if (!ptr_type) {
     Fail() << "invalid texture type for " << image.PrettyPrint();
-    return nullptr;
+    return {};
   }
-  return As<sem::Texture>(ptr_type->type()->UnwrapAll());
+  auto result = typ::As<typ::Texture>(UnwrapAll(typ::Call_type(ptr_type)));
+  if (!result) {
+    Fail() << "invalid texture type for " << image.PrettyPrint();
+    return {};
+  }
+  return result;
 }
 
 ast::Expression* FunctionEmitter::GetImageExpression(
@@ -4449,12 +4461,13 @@
     }
   }
 
-  sem::Pointer* texture_ptr_type = parser_impl_.GetTypeForHandleVar(*image);
+  typ::Pointer texture_ptr_type = parser_impl_.GetTypeForHandleVar(*image);
   if (!texture_ptr_type) {
     return Fail();
   }
-  sem::Texture* texture_type =
-      texture_ptr_type->type()->UnwrapAll()->As<sem::Texture>();
+  typ::Texture texture_type =
+      typ::As<typ::Texture>(UnwrapAll(typ::Call_type(texture_ptr_type)));
+
   if (!texture_type) {
     return Fail();
   }
@@ -4556,7 +4569,7 @@
     }
     TypedExpression lod = MakeOperand(inst, arg_index);
     // When sampling from a depth texture, the Lod operand must be an I32.
-    if (texture_type->Is<sem::DepthTexture>()) {
+    if (texture_type.ast->Is<ast::DepthTexture>()) {
       // Convert it to a signed integer type.
       lod = ToI32(lod);
     }
@@ -4564,11 +4577,11 @@
     image_operands_mask ^= SpvImageOperandsLodMask;
     arg_index++;
   } else if ((opcode == SpvOpImageFetch) &&
-             (texture_type->Is<sem::SampledTexture>() ||
-              texture_type->Is<sem::DepthTexture>())) {
+             (texture_type.ast->Is<ast::SampledTexture>() ||
+              texture_type.ast->Is<ast::DepthTexture>())) {
     // textureLoad on sampled texture and depth texture requires an explicit
     // level-of-detail parameter.
-    params.push_back(parser_impl_.MakeNullValue(i32_));
+    params.push_back(parser_impl_.MakeNullValue(builder_.ty.i32()));
   }
   if (arg_index + 1 < num_args &&
       (image_operands_mask & SpvImageOperandsGradMask)) {
@@ -4626,10 +4639,10 @@
     ast::Expression* value = call_expr;
 
     // The result type, derived from the SPIR-V instruction.
-    auto* result_type = parser_impl_.ConvertType(inst.type_id());
-    auto* result_component_type = result_type;
-    if (auto* result_vector_type = result_type->As<sem::Vector>()) {
-      result_component_type = result_vector_type->type();
+    auto result_type = parser_impl_.ConvertType(inst.type_id());
+    auto result_component_type = result_type;
+    if (auto result_vector_type = typ::As<typ::Vector>(result_type)) {
+      result_component_type = typ::Call_type(result_vector_type);
     }
 
     // For depth textures, the arity might mot match WGSL:
@@ -4643,7 +4656,7 @@
     //   dref gather         vec4  ImageFetch           vec4 TODO(dneto)
     // Construct a 4-element vector with the result from the builtin in the
     // first component.
-    if (texture_type->Is<sem::DepthTexture>()) {
+    if (texture_type.ast->Is<ast::DepthTexture>()) {
       if (is_non_dref_sample || (opcode == SpvOpImageFetch)) {
         value = create<ast::TypeConstructorExpression>(
             Source{},
@@ -4664,14 +4677,14 @@
       return Fail() << "invalid image type for image memory object declaration "
                     << image->PrettyPrint();
     }
-    auto* expected_component_type =
+    auto expected_component_type =
         parser_impl_.ConvertType(spirv_image_type->GetSingleWordInOperand(0));
     if (expected_component_type != result_component_type) {
       // This occurs if one is signed integer and the other is unsigned integer,
       // or vice versa. Perform a bitcast.
       value = create<ast::BitcastExpression>(Source{}, result_type, call_expr);
     }
-    if (!expected_component_type->Is<sem::F32>() &&
+    if (!expected_component_type.ast->Is<ast::F32>() &&
         IsSampledImageAccess(opcode)) {
       // WGSL permits sampled image access only on float textures.
       // Reject this case in the SPIR-V reader, at least until SPIR-V validation
@@ -4694,7 +4707,7 @@
   if (!image) {
     return false;
   }
-  auto* texture_type = GetImageType(*image);
+  auto texture_type = GetImageType(*image);
   if (!texture_type) {
     return false;
   }
@@ -4722,7 +4735,7 @@
             Source{}, layers_ident,
             ast::ExpressionList{GetImageExpression(inst)}));
       }
-      auto* result_type = parser_impl_.ConvertType(inst.type_id());
+      auto result_type = parser_impl_.ConvertType(inst.type_id());
       TypedExpression expr = {
           result_type,
           create<ast::TypeConstructorExpression>(Source{}, result_type, exprs)};
@@ -4742,10 +4755,10 @@
       ast::Expression* ast_expr = create<ast::CallExpression>(
           Source{}, levels_ident,
           ast::ExpressionList{GetImageExpression(inst)});
-      auto* result_type = parser_impl_.ConvertType(inst.type_id());
+      auto result_type = parser_impl_.ConvertType(inst.type_id());
       // The SPIR-V result type must be integer scalar. The WGSL bulitin
       // returns i32. If they aren't the same then convert the result.
-      if (result_type != i32_) {
+      if (result_type != builder_.ty.i32()) {
         ast_expr = create<ast::TypeConstructorExpression>(
             Source{}, result_type, ast::ExpressionList{ast_expr});
       }
@@ -4790,7 +4803,7 @@
   if (!raw_coords.type) {
     return {};
   }
-  sem::Texture* texture_type = GetImageType(*image);
+  typ::Texture texture_type = GetImageType(*image);
   if (!texture_type) {
     return {};
   }
@@ -4805,12 +4818,12 @@
   }
   const auto num_coords_required = num_axes + (is_arrayed ? 1 : 0);
   uint32_t num_coords_supplied = 0;
-  auto* component_type = raw_coords.type;
+  auto component_type = raw_coords.type;
   if (component_type->is_float_scalar() ||
       component_type->is_integer_scalar()) {
     num_coords_supplied = 1;
-  } else if (auto* vec_type = raw_coords.type->As<sem::Vector>()) {
-    component_type = vec_type->type();
+  } else if (auto vec_type = typ::As<typ::Vector>(raw_coords.type)) {
+    component_type = typ::Call_type(vec_type);
     num_coords_supplied = vec_type->size();
   }
   if (num_coords_supplied == 0) {
@@ -4834,9 +4847,9 @@
   // will actually use them.
   auto prefix_swizzle_expr = [this, num_axes, component_type,
                               raw_coords]() -> ast::Expression* {
-    auto* swizzle_type = (num_axes == 1)
-                             ? component_type
-                             : create<sem::Vector>(component_type, num_axes);
+    auto swizzle_type =
+        (num_axes == 1) ? component_type
+                        : typ::Type{builder_.ty.vec(component_type, num_axes)};
     auto* swizzle = create<ast::MemberAccessorExpression>(
         Source{}, raw_coords.expr, PrefixSwizzle(num_axes));
     return ToSignedIfUnsigned({swizzle_type, swizzle}).expr;
@@ -4870,15 +4883,15 @@
 ast::Expression* FunctionEmitter::ConvertTexelForStorage(
     const spvtools::opt::Instruction& inst,
     TypedExpression texel,
-    sem::Texture* texture_type) {
-  auto* storage_texture_type = texture_type->As<sem::StorageTexture>();
-  auto* src_type = texel.type;
+    typ::Texture texture_type) {
+  auto storage_texture_type = typ::As<typ::StorageTexture>(texture_type);
+  auto src_type = texel.type;
   if (!storage_texture_type) {
     Fail() << "writing to other than storage texture: " << inst.PrettyPrint();
     return nullptr;
   }
   const auto format = storage_texture_type->image_format();
-  auto* dest_type = parser_impl_.GetTexelTypeForFormat(format);
+  auto dest_type = parser_impl_.GetTexelTypeForFormat(format);
   if (!dest_type) {
     Fail();
     return nullptr;
@@ -4888,14 +4901,14 @@
   }
 
   const uint32_t dest_count =
-      dest_type->is_scalar() ? 1 : dest_type->As<sem::Vector>()->size();
+      dest_type->is_scalar() ? 1 : dest_type.ast->As<ast::Vector>()->size();
   if (dest_count == 3) {
     Fail() << "3-channel storage textures are not supported: "
            << inst.PrettyPrint();
     return nullptr;
   }
   const uint32_t src_count =
-      src_type->is_scalar() ? 1 : src_type->As<sem::Vector>()->size();
+      src_type->is_scalar() ? 1 : src_type.ast->As<ast::Vector>()->size();
   if (src_count < dest_count) {
     Fail() << "texel has too few components for storage texture: " << src_count
            << " provided but " << dest_count
@@ -4954,21 +4967,22 @@
 }
 
 TypedExpression FunctionEmitter::ToI32(TypedExpression value) {
-  if (!value.type || value.type == i32_) {
+  if (!value.type || value.type == builder_.ty.i32()) {
     return value;
   }
-  return {i32_, create<ast::TypeConstructorExpression>(
-                    Source{}, i32_, ast::ExpressionList{value.expr})};
+  return {builder_.ty.i32(),
+          create<ast::TypeConstructorExpression>(
+              Source{}, builder_.ty.i32(), ast::ExpressionList{value.expr})};
 }
 
 TypedExpression FunctionEmitter::ToSignedIfUnsigned(TypedExpression value) {
   if (!value.type || !value.type->is_unsigned_scalar_or_vector()) {
     return value;
   }
-  if (auto* vec_type = value.type->As<sem::Vector>()) {
-    auto* new_type = create<sem::Vector>(i32_, vec_type->size());
-    return {new_type, create<ast::TypeConstructorExpression>(
-                          Source{}, new_type, ast::ExpressionList{value.expr})};
+  if (auto* vec_type = value.type.ast->As<ast::Vector>()) {
+    auto new_type = builder_.ty.vec(builder_.ty.i32(), vec_type->size());
+    return {new_type,
+            builder_.Construct(new_type, ast::ExpressionList{value.expr})};
   }
   return ToI32(value);
 }
@@ -5021,9 +5035,10 @@
   // Synthesize the result.
   auto col = MakeOperand(inst, 0);
   auto row = MakeOperand(inst, 1);
-  auto* col_ty = col.type->As<sem::Vector>();
-  auto* row_ty = row.type->As<sem::Vector>();
-  auto* result_ty = parser_impl_.ConvertType(inst.type_id())->As<sem::Matrix>();
+  auto col_ty = typ::As<typ::Vector>(col.type);
+  auto row_ty = typ::As<typ::Vector>(row.type);
+  auto result_ty =
+      typ::As<typ::Matrix>(parser_impl_.ConvertType(inst.type_id()));
   if (!col_ty || !col_ty || !result_ty || result_ty->type() != col_ty->type() ||
       result_ty->type() != row_ty->type() ||
       result_ty->columns() != row_ty->size() ||
@@ -5074,7 +5089,7 @@
   // Then use result everywhere the original SPIR-V id is used.  Using a const
   // like this avoids constantly reloading the value many times.
 
-  auto* ast_type = parser_impl_.ConvertType(inst.type_id());
+  auto ast_type = parser_impl_.ConvertType(inst.type_id());
   auto src_vector = MakeOperand(inst, 0);
   auto component = MakeOperand(inst, 1);
   auto index = MakeOperand(inst, 2);
@@ -5121,7 +5136,7 @@
   // - building up an access-chain like access like for CompositeExtract, but
   //   on the left-hand side of the assignment.
 
-  auto* ast_type = parser_impl_.ConvertType(inst.type_id());
+  auto ast_type = parser_impl_.ConvertType(inst.type_id());
   auto component = MakeOperand(inst, 0);
   auto src_composite = MakeOperand(inst, 1);
 
diff --git a/src/reader/spirv/function.h b/src/reader/spirv/function.h
index 778de9a..24393dd 100644
--- a/src/reader/spirv/function.h
+++ b/src/reader/spirv/function.h
@@ -25,6 +25,7 @@
 #include "src/program_builder.h"
 #include "src/reader/spirv/construct.h"
 #include "src/reader/spirv/parser_impl.h"
+#include "src/typepair.h"
 
 namespace tint {
 namespace reader {
@@ -512,7 +513,7 @@
   /// @param type the AST type
   /// @param result_id the SPIR-V ID for the locally defined value
   /// @returns an possibly updated type
-  sem::Type* RemapStorageClass(sem::Type* type, uint32_t result_id);
+  typ::Type RemapStorageClass(typ::Type type, uint32_t result_id);
 
   /// Marks locally defined values when they should get a 'const'
   /// definition in WGSL, or a 'var' definition at an outer scope.
@@ -853,7 +854,7 @@
     /// Function parameters
     ast::VariableList params;
     /// Function return type
-    sem::Type* return_type;
+    typ::Type return_type;
     /// Function decorations
     ast::DecorationList decorations;
   };
@@ -866,7 +867,7 @@
 
   /// @returns the store type for the OpVariable instruction, or
   /// null on failure.
-  sem::Type* GetVariableStoreType(
+  typ::Type GetVariableStoreType(
       const spvtools::opt::Instruction& var_decl_inst);
 
   /// Returns an expression for an instruction operand. Signedness conversion is
@@ -934,7 +935,7 @@
   /// Get the AST texture the SPIR-V image memory object declaration.
   /// @param inst the SPIR-V memory object declaration for the image.
   /// @returns a texture type, or null on error
-  sem::Texture* GetImageType(const spvtools::opt::Instruction& inst);
+  typ::Texture GetImageType(const spvtools::opt::Instruction& inst);
 
   /// Get the expression for the image operand from the first operand to the
   /// given instruction.
@@ -971,7 +972,7 @@
   ast::Expression* ConvertTexelForStorage(
       const spvtools::opt::Instruction& inst,
       TypedExpression texel,
-      sem::Texture* texture_type);
+      typ::Texture texture_type);
 
   /// Returns an expression for an OpSelect, if its operands are scalars
   /// or vectors. These translate directly to WGSL select.  Otherwise, return
@@ -1133,8 +1134,6 @@
   FailStream& fail_stream_;
   Namer& namer_;
   const spvtools::opt::Function& function_;
-  sem::I32* const i32_;  // The unique I32 type object.
-  sem::U32* const u32_;  // The unique U32 type object.
 
   // The SPIR-V ID for the SampleMask input variable.
   uint32_t sample_mask_in_id;
diff --git a/src/reader/spirv/parser_impl.cc b/src/reader/spirv/parser_impl.cc
index fe246ba..cbe0456 100644
--- a/src/reader/spirv/parser_impl.cc
+++ b/src/reader/spirv/parser_impl.cc
@@ -23,6 +23,7 @@
 #include "src/ast/bitcast_expression.h"
 #include "src/ast/override_decoration.h"
 #include "src/ast/struct_block_decoration.h"
+#include "src/ast/type_name.h"
 #include "src/reader/spirv/function.h"
 #include "src/sem/access_control_type.h"
 #include "src/sem/depth_texture_type.h"
@@ -228,13 +229,28 @@
   return false;
 }
 
+// Forwards UnwrapIfNeeded to both the ast and sem types of the TypePair
+// @param tp the type pair
+// @returns the unwrapped type pair
+typ::Type UnwrapIfNeeded(typ::Type tp) {
+  return typ::Type{tp.ast->UnwrapIfNeeded(), tp.sem->UnwrapIfNeeded()};
+}
+
 }  // namespace
 
+TypedExpression::TypedExpression() = default;
+
+TypedExpression::TypedExpression(const TypedExpression&) = default;
+
+TypedExpression& TypedExpression::operator=(const TypedExpression&) = default;
+
+TypedExpression::TypedExpression(typ::Type type_in, ast::Expression* expr_in)
+    : type(type_in), expr(expr_in) {}
+
 ParserImpl::ParserImpl(const std::vector<uint32_t>& spv_binary)
     : Reader(),
       spv_binary_(spv_binary),
       fail_stream_(&success_, &errors_),
-      bool_type_(builder_.create<sem::Bool>()),
       namer_(fail_stream_),
       enum_converter_(fail_stream_),
       tools_context_(kInputEnv) {
@@ -292,7 +308,7 @@
   return tint::Program(std::move(builder_));
 }
 
-sem::Type* ParserImpl::ConvertType(uint32_t type_id) {
+typ::Type ParserImpl::ConvertType(uint32_t type_id) {
   if (!success_) {
     return nullptr;
   }
@@ -302,46 +318,42 @@
     return nullptr;
   }
 
-  auto where = id_to_type_.find(type_id);
-  if (where != id_to_type_.end()) {
-    return where->second;
-  }
-
   auto* spirv_type = type_mgr_->GetType(type_id);
   if (spirv_type == nullptr) {
     Fail() << "ID is not a SPIR-V type: " << type_id;
     return nullptr;
   }
 
-  auto save = [this, type_id, spirv_type](sem::Type* type) {
+  auto maybe_generate_alias = [this, type_id,
+                               spirv_type](typ::Type type) -> typ::Type {
     if (type != nullptr) {
-      id_to_type_[type_id] = type;
-      MaybeGenerateAlias(type_id, spirv_type);
+      return MaybeGenerateAlias(type_id, spirv_type, type);
     }
-    return type;
+    return {};
   };
 
   switch (spirv_type->kind()) {
     case spvtools::opt::analysis::Type::kVoid:
-      return save(builder_.create<sem::Void>());
+      return maybe_generate_alias(builder_.ty.void_());
     case spvtools::opt::analysis::Type::kBool:
-      return save(bool_type_);
+      return maybe_generate_alias(builder_.ty.bool_());
     case spvtools::opt::analysis::Type::kInteger:
-      return save(ConvertType(spirv_type->AsInteger()));
+      return maybe_generate_alias(ConvertType(spirv_type->AsInteger()));
     case spvtools::opt::analysis::Type::kFloat:
-      return save(ConvertType(spirv_type->AsFloat()));
+      return maybe_generate_alias(ConvertType(spirv_type->AsFloat()));
     case spvtools::opt::analysis::Type::kVector:
-      return save(ConvertType(spirv_type->AsVector()));
+      return maybe_generate_alias(ConvertType(spirv_type->AsVector()));
     case spvtools::opt::analysis::Type::kMatrix:
-      return save(ConvertType(spirv_type->AsMatrix()));
+      return maybe_generate_alias(ConvertType(spirv_type->AsMatrix()));
     case spvtools::opt::analysis::Type::kRuntimeArray:
-      return save(ConvertType(spirv_type->AsRuntimeArray()));
+      return maybe_generate_alias(ConvertType(spirv_type->AsRuntimeArray()));
     case spvtools::opt::analysis::Type::kArray:
-      return save(ConvertType(spirv_type->AsArray()));
+      return maybe_generate_alias(ConvertType(spirv_type->AsArray()));
     case spvtools::opt::analysis::Type::kStruct:
-      return save(ConvertType(type_id, spirv_type->AsStruct()));
+      return maybe_generate_alias(ConvertType(type_id, spirv_type->AsStruct()));
     case spvtools::opt::analysis::Type::kPointer:
-      return save(ConvertType(type_id, spirv_type->AsPointer()));
+      return maybe_generate_alias(
+          ConvertType(type_id, spirv_type->AsPointer()));
     case spvtools::opt::analysis::Type::kFunction:
       // Tint doesn't have a Function type.
       // We need to convert the result type and parameter types.
@@ -353,7 +365,7 @@
     case spvtools::opt::analysis::Type::kImage:
       // Fake it for sampler and texture types.  These are handled in an
       // entirely different way.
-      return save(builder_.create<sem::Void>());
+      return maybe_generate_alias(builder_.ty.void_());
     default:
       break;
   }
@@ -765,67 +777,51 @@
   return success_;
 }
 
-sem::Type* ParserImpl::ConvertType(
+typ::Type ParserImpl::ConvertType(
     const spvtools::opt::analysis::Integer* int_ty) {
   if (int_ty->width() == 32) {
-    sem::Type* signed_ty = builder_.create<sem::I32>();
-    sem::Type* unsigned_ty = builder_.create<sem::U32>();
-    signed_type_for_[unsigned_ty] = signed_ty;
-    unsigned_type_for_[signed_ty] = unsigned_ty;
-    return int_ty->IsSigned() ? signed_ty : unsigned_ty;
+    return int_ty->IsSigned() ? typ::Type{builder_.ty.i32()}
+                              : typ::Type{builder_.ty.u32()};
   }
   Fail() << "unhandled integer width: " << int_ty->width();
   return nullptr;
 }
 
-sem::Type* ParserImpl::ConvertType(
+typ::Type ParserImpl::ConvertType(
     const spvtools::opt::analysis::Float* float_ty) {
   if (float_ty->width() == 32) {
-    return builder_.create<sem::F32>();
+    return builder_.ty.f32();
   }
   Fail() << "unhandled float width: " << float_ty->width();
   return nullptr;
 }
 
-sem::Type* ParserImpl::ConvertType(
+typ::Type ParserImpl::ConvertType(
     const spvtools::opt::analysis::Vector* vec_ty) {
   const auto num_elem = vec_ty->element_count();
-  auto* ast_elem_ty = ConvertType(type_mgr_->GetId(vec_ty->element_type()));
+  auto ast_elem_ty = ConvertType(type_mgr_->GetId(vec_ty->element_type()));
   if (ast_elem_ty == nullptr) {
     return nullptr;
   }
-  auto* this_ty = builder_.create<sem::Vector>(ast_elem_ty, num_elem);
-  // Generate the opposite-signedness vector type, if this type is integral.
-  if (unsigned_type_for_.count(ast_elem_ty)) {
-    auto* other_ty =
-        builder_.create<sem::Vector>(unsigned_type_for_[ast_elem_ty], num_elem);
-    signed_type_for_[other_ty] = this_ty;
-    unsigned_type_for_[this_ty] = other_ty;
-  } else if (signed_type_for_.count(ast_elem_ty)) {
-    auto* other_ty =
-        builder_.create<sem::Vector>(signed_type_for_[ast_elem_ty], num_elem);
-    unsigned_type_for_[other_ty] = this_ty;
-    signed_type_for_[this_ty] = other_ty;
-  }
-  return this_ty;
+  return builder_.ty.vec(ast_elem_ty, num_elem);
 }
 
-sem::Type* ParserImpl::ConvertType(
+typ::Type ParserImpl::ConvertType(
     const spvtools::opt::analysis::Matrix* mat_ty) {
   const auto* vec_ty = mat_ty->element_type()->AsVector();
   const auto* scalar_ty = vec_ty->element_type();
   const auto num_rows = vec_ty->element_count();
   const auto num_columns = mat_ty->element_count();
-  auto* ast_scalar_ty = ConvertType(type_mgr_->GetId(scalar_ty));
+  auto ast_scalar_ty = ConvertType(type_mgr_->GetId(scalar_ty));
   if (ast_scalar_ty == nullptr) {
     return nullptr;
   }
-  return builder_.create<sem::Matrix>(ast_scalar_ty, num_rows, num_columns);
+  return builder_.ty.mat(ast_scalar_ty, num_columns, num_rows);
 }
 
-sem::Type* ParserImpl::ConvertType(
+typ::Type ParserImpl::ConvertType(
     const spvtools::opt::analysis::RuntimeArray* rtarr_ty) {
-  auto* ast_elem_ty = ConvertType(type_mgr_->GetId(rtarr_ty->element_type()));
+  auto ast_elem_ty = ConvertType(type_mgr_->GetId(rtarr_ty->element_type()));
   if (ast_elem_ty == nullptr) {
     return nullptr;
   }
@@ -833,13 +829,13 @@
   if (!ParseArrayDecorations(rtarr_ty, &decorations)) {
     return nullptr;
   }
-  return create<sem::ArrayType>(ast_elem_ty, 0, std::move(decorations));
+  return builder_.ty.array(ast_elem_ty, 0, std::move(decorations));
 }
 
-sem::Type* ParserImpl::ConvertType(
+typ::Type ParserImpl::ConvertType(
     const spvtools::opt::analysis::Array* arr_ty) {
   const auto elem_type_id = type_mgr_->GetId(arr_ty->element_type());
-  auto* ast_elem_ty = ConvertType(elem_type_id);
+  auto ast_elem_ty = ConvertType(elem_type_id);
   if (ast_elem_ty == nullptr) {
     return nullptr;
   }
@@ -878,8 +874,8 @@
   if (remap_buffer_block_type_.count(elem_type_id)) {
     remap_buffer_block_type_.insert(type_mgr_->GetId(arr_ty));
   }
-  return create<sem::ArrayType>(ast_elem_ty, static_cast<uint32_t>(num_elem),
-                                std::move(decorations));
+  return builder_.ty.array(ast_elem_ty, static_cast<uint32_t>(num_elem),
+                           std::move(decorations));
 }
 
 bool ParserImpl::ParseArrayDecorations(
@@ -911,7 +907,7 @@
   return true;
 }
 
-sem::Type* ParserImpl::ConvertType(
+typ::Type ParserImpl::ConvertType(
     uint32_t type_id,
     const spvtools::opt::analysis::Struct* struct_ty) {
   // Compute the struct decoration.
@@ -941,7 +937,7 @@
   for (uint32_t member_index = 0; member_index < members.size();
        ++member_index) {
     const auto member_type_id = type_mgr_->GetId(members[member_index]);
-    auto* ast_member_ty = ConvertType(member_type_id);
+    auto ast_member_ty = ConvertType(member_type_id);
     if (ast_member_ty == nullptr) {
       // Already emitted diagnostics.
       return nullptr;
@@ -1033,17 +1029,16 @@
   }
   auto* ast_struct = create<ast::Struct>(Source{}, sym, std::move(ast_members),
                                          std::move(ast_struct_decorations));
-  auto* result = builder_.create<sem::StructType>(ast_struct);
-  id_to_type_[type_id] = result;
+  auto result = builder_.ty.struct_(ast_struct);
   if (num_non_writable_members == members.size()) {
-    read_only_struct_types_.insert(result);
+    read_only_struct_types_.insert(result.ast->name());
   }
   builder_.AST().AddConstructedType(result);
   return result;
 }
 
-sem::Type* ParserImpl::ConvertType(uint32_t type_id,
-                                   const spvtools::opt::analysis::Pointer*) {
+typ::Type ParserImpl::ConvertType(uint32_t type_id,
+                                  const spvtools::opt::analysis::Pointer*) {
   const auto* inst = def_use_mgr_->GetDef(type_id);
   const auto pointee_type_id = inst->GetSingleWordInOperand(1);
   const auto storage_class = SpvStorageClass(inst->GetSingleWordInOperand(0));
@@ -1053,7 +1048,7 @@
     builtin_position_.storage_class = storage_class;
     return nullptr;
   }
-  auto* ast_elem_ty = ConvertType(pointee_type_id);
+  auto ast_elem_ty = ConvertType(pointee_type_id);
   if (ast_elem_ty == nullptr) {
     Fail() << "SPIR-V pointer type with ID " << type_id
            << " has invalid pointee type " << pointee_type_id;
@@ -1082,7 +1077,8 @@
     }
   }
 
-  return builder_.create<sem::Pointer>(ast_elem_ty, ast_storage_class);
+  ast_elem_ty = builder_.ty.MaybeCreateTypename(ast_elem_ty);
+  return builder_.ty.pointer(ast_elem_ty, ast_storage_class);
 }
 
 bool ParserImpl::RegisterTypes() {
@@ -1115,7 +1111,7 @@
   // that is OpSpecConstantTrue, OpSpecConstantFalse, or OpSpecConstant.
   for (auto& inst : module_->types_values()) {
     // These will be populated for a valid scalar spec constant.
-    sem::Type* ast_type = nullptr;
+    typ::Type ast_type;
     ast::ScalarConstructorExpression* ast_expr = nullptr;
 
     switch (inst.opcode()) {
@@ -1130,15 +1126,15 @@
       case SpvOpSpecConstant: {
         ast_type = ConvertType(inst.type_id());
         const uint32_t literal_value = inst.GetSingleWordInOperand(0);
-        if (ast_type->Is<sem::I32>()) {
+        if (ast_type.ast->Is<ast::I32>()) {
           ast_expr = create<ast::ScalarConstructorExpression>(
               Source{}, create<ast::SintLiteral>(
                             Source{}, static_cast<int32_t>(literal_value)));
-        } else if (ast_type->Is<sem::U32>()) {
+        } else if (ast_type.ast->Is<ast::U32>()) {
           ast_expr = create<ast::ScalarConstructorExpression>(
               Source{}, create<ast::UintLiteral>(
                             Source{}, static_cast<uint32_t>(literal_value)));
-        } else if (ast_type->Is<sem::F32>()) {
+        } else if (ast_type.ast->Is<ast::F32>()) {
           float float_value;
           // Copy the bits so we can read them as a float.
           std::memcpy(&float_value, &literal_value, sizeof(float_value));
@@ -1174,10 +1170,12 @@
   return success_;
 }
 
-void ParserImpl::MaybeGenerateAlias(uint32_t type_id,
-                                    const spvtools::opt::analysis::Type* type) {
+typ::Type ParserImpl::MaybeGenerateAlias(
+    uint32_t type_id,
+    const spvtools::opt::analysis::Type* type,
+    typ::Type ast_type) {
   if (!success_) {
-    return;
+    return {};
   }
 
   // We only care about arrays, and runtime arrays.
@@ -1190,25 +1188,27 @@
     case spvtools::opt::analysis::Type::kArray:
       // Only make a type aliase for arrays with decorations.
       if (GetDecorationsFor(type_id).empty()) {
-        return;
+        return ast_type;
       }
       namer_.SuggestSanitizedName(type_id, "Arr");
       break;
     default:
       // Ignore constants, and any other types.
-      return;
+      return ast_type;
   }
-  auto* ast_underlying_type = id_to_type_[type_id];
+  auto ast_underlying_type = ast_type;
   if (ast_underlying_type == nullptr) {
     Fail() << "internal error: no type registered for SPIR-V ID: " << type_id;
-    return;
+    return {};
   }
   const auto name = namer_.GetName(type_id);
-  auto* ast_alias_type = builder_.create<sem::Alias>(
-      builder_.Symbols().Register(name), ast_underlying_type);
+  auto ast_alias_type =
+      builder_.ty.alias(builder_.Symbols().Register(name), ast_underlying_type);
+
   // Record this new alias as the AST type for this SPIR-V ID.
-  id_to_type_[type_id] = ast_alias_type;
   builder_.AST().AddConstructedType(ast_alias_type);
+
+  return ast_alias_type;
 }
 
 bool ParserImpl::EmitModuleScopeVariables() {
@@ -1249,7 +1249,7 @@
     if (!success_) {
       return false;
     }
-    sem::Type* ast_type = nullptr;
+    typ::Type ast_type;
     if (spirv_storage_class == SpvStorageClassUniformConstant) {
       // These are opaque handles: samplers or textures
       ast_type = GetTypeForHandleVar(var);
@@ -1257,20 +1257,20 @@
         return false;
       }
     } else {
-      ast_type = id_to_type_[type_id];
+      ast_type = ConvertType(type_id);
       if (ast_type == nullptr) {
         return Fail() << "internal error: failed to register Tint AST type for "
                          "SPIR-V type with ID: "
                       << var.type_id();
       }
-      if (!ast_type->Is<sem::Pointer>()) {
+      if (!ast_type.ast->Is<ast::Pointer>()) {
         return Fail() << "variable with ID " << var.result_id()
                       << " has non-pointer type " << var.type_id();
       }
     }
 
-    auto* ast_store_type = ast_type->As<sem::Pointer>()->type();
-    auto ast_storage_class = ast_type->As<sem::Pointer>()->storage_class();
+    auto ast_store_type = typ::Call_type(typ::As<typ::Pointer>(ast_type));
+    auto ast_storage_class = ast_type.ast->As<ast::Pointer>()->storage_class();
     ast::Expression* ast_constructor = nullptr;
     if (var.NumInOperands() > 1) {
       // SPIR-V initializers are always constants.
@@ -1333,7 +1333,7 @@
 
 ast::Variable* ParserImpl::MakeVariable(uint32_t id,
                                         ast::StorageClass sc,
-                                        sem::Type* type,
+                                        typ::Type type,
                                         bool is_const,
                                         ast::Expression* constructor,
                                         ast::DecorationList decorations) {
@@ -1343,11 +1343,15 @@
   }
 
   if (sc == ast::StorageClass::kStorage) {
+    bool read_only = false;
+    if (auto* tn = type.ast->As<ast::TypeName>()) {
+      read_only = read_only_struct_types_.count(tn->name()) > 0;
+    }
+
     // Apply the access(read) or access(read_write) modifier.
-    auto access = read_only_struct_types_.count(type)
-                      ? ast::AccessControl::kReadOnly
-                      : ast::AccessControl::kReadWrite;
-    type = builder_.create<sem::AccessControl>(access, type);
+    auto access = read_only ? ast::AccessControl::kReadOnly
+                            : ast::AccessControl::kReadWrite;
+    type = builder_.ty.access(access, type);
   }
 
   for (auto& deco : GetDecorationsFor(id)) {
@@ -1372,7 +1376,7 @@
           // The SPIR-V variable is likely to be signed (because GLSL
           // requires signed), but WGSL requires unsigned.  Handle specially
           // so we always perform the conversion at load and store.
-          if (auto* forced_type = unsigned_type_for_[type]) {
+          if (auto forced_type = UnsignedTypeFor(type)) {
             // Requires conversion and special handling in code generation.
             special_builtins_[id] = spv_builtin;
             type = forced_type;
@@ -1389,7 +1393,7 @@
                       "SampleMask must be an array of 1 element.";
           }
           special_builtins_[id] = spv_builtin;
-          type = builder_.create<sem::U32>();
+          type = builder_.ty.u32();
           break;
         }
         default:
@@ -1449,7 +1453,7 @@
     Fail() << "ID " << id << " is not a registered instruction";
     return {};
   }
-  auto* original_ast_type = ConvertType(inst->type_id());
+  auto original_ast_type = ConvertType(inst->type_id());
   if (original_ast_type == nullptr) {
     return {};
   }
@@ -1467,28 +1471,28 @@
   }
 
   auto source = GetSourceForInst(inst);
-  auto* ast_type = original_ast_type->UnwrapIfNeeded();
+  auto ast_type = UnwrapIfNeeded(original_ast_type);
 
   // TODO(dneto): Note: NullConstant for int, uint, float map to a regular 0.
   // So canonicalization should map that way too.
   // Currently "null<type>" is missing from the WGSL parser.
   // See https://bugs.chromium.org/p/tint/issues/detail?id=34
-  if (ast_type->Is<sem::U32>()) {
+  if (ast_type.ast->Is<ast::U32>()) {
     return {ast_type, create<ast::ScalarConstructorExpression>(
                           Source{}, create<ast::UintLiteral>(
                                         source, spirv_const->GetU32()))};
   }
-  if (ast_type->Is<sem::I32>()) {
+  if (ast_type.ast->Is<ast::I32>()) {
     return {ast_type, create<ast::ScalarConstructorExpression>(
                           Source{}, create<ast::SintLiteral>(
                                         source, spirv_const->GetS32()))};
   }
-  if (ast_type->Is<sem::F32>()) {
+  if (ast_type.ast->Is<ast::F32>()) {
     return {ast_type, create<ast::ScalarConstructorExpression>(
                           Source{}, create<ast::FloatLiteral>(
                                         source, spirv_const->GetFloat()))};
   }
-  if (ast_type->Is<sem::Bool>()) {
+  if (ast_type.ast->Is<ast::Bool>()) {
     const bool value = spirv_const->AsNullConstant()
                            ? false
                            : spirv_const->AsBoolConstant()->value();
@@ -1531,7 +1535,7 @@
   return {};
 }
 
-ast::Expression* ParserImpl::MakeNullValue(sem::Type* type) {
+ast::Expression* ParserImpl::MakeNullValue(typ::Type type) {
   // TODO(dneto): Use the no-operands constructor syntax when it becomes
   // available in Tint.
   // https://github.com/gpuweb/gpuweb/issues/685
@@ -1542,37 +1546,42 @@
     return nullptr;
   }
 
-  auto* original_type = type;
-  type = type->UnwrapIfNeeded();
+  auto original_type = type;
+  type = UnwrapIfNeeded(type);
 
-  if (type->Is<sem::Bool>()) {
+  if (type.ast->Is<ast::Bool>()) {
     return create<ast::ScalarConstructorExpression>(
         Source{}, create<ast::BoolLiteral>(Source{}, false));
   }
-  if (type->Is<sem::U32>()) {
+  if (type.ast->Is<ast::U32>()) {
     return create<ast::ScalarConstructorExpression>(
         Source{}, create<ast::UintLiteral>(Source{}, 0u));
   }
-  if (type->Is<sem::I32>()) {
+  if (type.ast->Is<ast::I32>()) {
     return create<ast::ScalarConstructorExpression>(
         Source{}, create<ast::SintLiteral>(Source{}, 0));
   }
-  if (type->Is<sem::F32>()) {
+  if (type.ast->Is<ast::F32>()) {
     return create<ast::ScalarConstructorExpression>(
         Source{}, create<ast::FloatLiteral>(Source{}, 0.0f));
   }
-  if (const auto* vec_ty = type->As<sem::Vector>()) {
+  if (type.ast->Is<ast::TypeName>()) {
+    // TODO(amaiorano): No type constructor for TypeName (yet?)
+    ast::ExpressionList ast_components;
+    return create<ast::TypeConstructorExpression>(Source{}, original_type,
+                                                  std::move(ast_components));
+  }
+  if (auto vec_ty = typ::As<typ::Vector>(type)) {
     ast::ExpressionList ast_components;
     for (size_t i = 0; i < vec_ty->size(); ++i) {
-      ast_components.emplace_back(MakeNullValue(vec_ty->type()));
+      ast_components.emplace_back(MakeNullValue(typ::Call_type(vec_ty)));
     }
     return create<ast::TypeConstructorExpression>(Source{}, type,
                                                   std::move(ast_components));
   }
-  if (const auto* mat_ty = type->As<sem::Matrix>()) {
+  if (auto mat_ty = typ::As<typ::Matrix>(type)) {
     // Matrix components are columns
-    auto* column_ty =
-        builder_.create<sem::Vector>(mat_ty->type(), mat_ty->rows());
+    auto column_ty = builder_.ty.vec(typ::Call_type(mat_ty), mat_ty->rows());
     ast::ExpressionList ast_components;
     for (size_t i = 0; i < mat_ty->columns(); ++i) {
       ast_components.emplace_back(MakeNullValue(column_ty));
@@ -1580,17 +1589,17 @@
     return create<ast::TypeConstructorExpression>(Source{}, type,
                                                   std::move(ast_components));
   }
-  if (auto* arr_ty = type->As<sem::ArrayType>()) {
+  if (auto arr_ty = typ::As<typ::Array>(type)) {
     ast::ExpressionList ast_components;
     for (size_t i = 0; i < arr_ty->size(); ++i) {
-      ast_components.emplace_back(MakeNullValue(arr_ty->type()));
+      ast_components.emplace_back(MakeNullValue(typ::Call_type(arr_ty)));
     }
     return create<ast::TypeConstructorExpression>(Source{}, original_type,
                                                   std::move(ast_components));
   }
-  if (auto* struct_ty = type->As<sem::StructType>()) {
+  if (auto struct_ty = typ::As<typ::Struct>(type)) {
     ast::ExpressionList ast_components;
-    for (auto* member : struct_ty->impl()->members()) {
+    for (auto* member : struct_ty.ast->members()) {
       ast_components.emplace_back(MakeNullValue(member->type()));
     }
     return create<ast::TypeConstructorExpression>(Source{}, original_type,
@@ -1600,10 +1609,34 @@
   return nullptr;
 }
 
-TypedExpression ParserImpl::MakeNullExpression(sem::Type* type) {
+TypedExpression ParserImpl::MakeNullExpression(typ::Type type) {
   return {type, MakeNullValue(type)};
 }
 
+typ::Type ParserImpl::UnsignedTypeFor(typ::Type type) {
+  if (type.ast->Is<ast::I32>()) {
+    return builder_.ty.u32();
+  }
+  if (auto* v = type.ast->As<ast::Vector>()) {
+    if (v->type()->Is<ast::I32>()) {
+      return builder_.ty.vec(builder_.ty.u32(), v->size());
+    }
+  }
+  return {};
+}
+
+typ::Type ParserImpl::SignedTypeFor(typ::Type type) {
+  if (type.ast->Is<ast::U32>()) {
+    return builder_.ty.i32();
+  }
+  if (auto* v = type.ast->As<ast::Vector>()) {
+    if (v->type()->Is<ast::U32>()) {
+      return builder_.ty.vec(builder_.ty.i32(), v->size());
+    }
+  }
+  return {};
+}
+
 TypedExpression ParserImpl::RectifyOperandSignedness(
     const spvtools::opt::Instruction& inst,
     TypedExpression&& expr) {
@@ -1627,22 +1660,20 @@
     Fail() << "internal error: RectifyOperandSignedness given a null expr\n";
     return {};
   }
-  auto* type = expr.type;
+  auto type = expr.type;
   if (!type) {
     Fail() << "internal error: unmapped type for: " << builder_.str(expr.expr)
            << "\n";
     return {};
   }
   if (requires_unsigned) {
-    auto* unsigned_ty = unsigned_type_for_[type];
-    if (unsigned_ty != nullptr) {
+    if (auto unsigned_ty = UnsignedTypeFor(type)) {
       // Conversion is required.
       return {unsigned_ty,
               create<ast::BitcastExpression>(Source{}, unsigned_ty, expr.expr)};
     }
   } else if (requires_signed) {
-    auto* signed_ty = signed_type_for_[type];
-    if (signed_ty != nullptr) {
+    if (auto signed_ty = SignedTypeFor(type)) {
       // Conversion is required.
       return {signed_ty,
               create<ast::BitcastExpression>(Source{}, signed_ty, expr.expr)};
@@ -1654,7 +1685,7 @@
 
 TypedExpression ParserImpl::RectifySecondOperandSignedness(
     const spvtools::opt::Instruction& inst,
-    sem::Type* first_operand_type,
+    typ::Type first_operand_type,
     TypedExpression&& second_operand_expr) {
   if ((first_operand_type != second_operand_expr.type) &&
       AssumesSecondOperandSignednessMatchesFirstOperand(inst.opcode())) {
@@ -1667,8 +1698,8 @@
   return std::move(second_operand_expr);
 }
 
-sem::Type* ParserImpl::ForcedResultType(const spvtools::opt::Instruction& inst,
-                                        sem::Type* first_operand_type) {
+typ::Type ParserImpl::ForcedResultType(const spvtools::opt::Instruction& inst,
+                                       typ::Type first_operand_type) {
   const auto opcode = inst.opcode();
   if (AssumesResultSignednessMatchesFirstOperand(opcode)) {
     return first_operand_type;
@@ -1683,34 +1714,36 @@
   return nullptr;
 }
 
-sem::Type* ParserImpl::GetSignedIntMatchingShape(sem::Type* other) {
+typ::Type ParserImpl::GetSignedIntMatchingShape(typ::Type other) {
   if (other == nullptr) {
     Fail() << "no type provided";
   }
-  auto* i32 = builder_.create<sem::I32>();
-  if (other->Is<sem::F32>() || other->Is<sem::U32>() || other->Is<sem::I32>()) {
+  auto i32 = builder_.ty.i32();
+  if (other.ast->Is<ast::F32>() || other.ast->Is<ast::U32>() ||
+      other.ast->Is<ast::I32>()) {
     return i32;
   }
-  auto* vec_ty = other->As<sem::Vector>();
+  auto* vec_ty = other.ast->As<ast::Vector>();
   if (vec_ty) {
-    return builder_.create<sem::Vector>(i32, vec_ty->size());
+    return builder_.ty.vec(i32, vec_ty->size());
   }
   Fail() << "required numeric scalar or vector, but got " << other->type_name();
   return nullptr;
 }
 
-sem::Type* ParserImpl::GetUnsignedIntMatchingShape(sem::Type* other) {
+typ::Type ParserImpl::GetUnsignedIntMatchingShape(typ::Type other) {
   if (other == nullptr) {
     Fail() << "no type provided";
     return nullptr;
   }
-  auto* u32 = builder_.create<sem::U32>();
-  if (other->Is<sem::F32>() || other->Is<sem::U32>() || other->Is<sem::I32>()) {
+  auto u32 = builder_.ty.u32();
+  if (other.ast->Is<ast::F32>() || other.ast->Is<ast::U32>() ||
+      other.ast->Is<ast::I32>()) {
     return u32;
   }
-  auto* vec_ty = other->As<sem::Vector>();
+  auto* vec_ty = other.ast->As<ast::Vector>();
   if (vec_ty) {
-    return builder_.create<sem::Vector>(u32, vec_ty->size());
+    return builder_.ty.vec(u32, vec_ty->size());
   }
   Fail() << "required numeric scalar or vector, but got " << other->type_name();
   return nullptr;
@@ -1719,8 +1752,8 @@
 TypedExpression ParserImpl::RectifyForcedResultType(
     TypedExpression expr,
     const spvtools::opt::Instruction& inst,
-    sem::Type* first_operand_type) {
-  auto* forced_result_ty = ForcedResultType(inst, first_operand_type);
+    typ::Type first_operand_type) {
+  auto forced_result_ty = ForcedResultType(inst, first_operand_type);
   if ((forced_result_ty == nullptr) || (forced_result_ty == expr.type)) {
     return expr;
   }
@@ -1898,7 +1931,7 @@
   return raw_handle_type;
 }
 
-sem::Pointer* ParserImpl::GetTypeForHandleVar(
+typ::Pointer ParserImpl::GetTypeForHandleVar(
     const spvtools::opt::Instruction& var) {
   auto where = handle_type_.find(&var);
   if (where != handle_type_.end()) {
@@ -1982,7 +2015,7 @@
   }
 
   // Construct the Tint handle type.
-  sem::Type* ast_store_type = nullptr;
+  typ::Type ast_store_type;
   if (usage.IsSampler()) {
     ast_store_type = builder_.ty.sampler(
         usage.IsComparisonSampler() ? ast::SamplerKind::kComparisonSampler
@@ -2007,7 +2040,7 @@
     if (usage.IsSampledTexture() ||
         (image_type->format() == SpvImageFormatUnknown)) {
       // Make a sampled texture type.
-      auto* ast_sampled_component_type =
+      auto ast_sampled_component_type =
           ConvertType(raw_handle_type->GetSingleWordInOperand(0));
 
       // Vulkan ignores the depth parameter on OpImage, so pay attention to the
@@ -2015,14 +2048,14 @@
       // OpImage variable with an OpImage*Dref* instruction.  In WGSL we must
       // treat that as a depth texture.
       if (image_type->depth() || usage.IsDepthTexture()) {
-        ast_store_type = builder_.create<sem::DepthTexture>(dim);
+        ast_store_type = builder_.ty.depth_texture(dim);
       } else if (image_type->is_multisampled()) {
         // Multisampled textures are never depth textures.
-        ast_store_type = builder_.create<sem::MultisampledTexture>(
-            dim, ast_sampled_component_type);
+        ast_store_type =
+            builder_.ty.multisampled_texture(dim, ast_sampled_component_type);
       } else {
-        ast_store_type = builder_.create<sem::SampledTexture>(
-            dim, ast_sampled_component_type);
+        ast_store_type =
+            builder_.ty.sampled_texture(dim, ast_sampled_component_type);
       }
     } else {
       const auto access = usage.IsStorageReadTexture()
@@ -2032,9 +2065,8 @@
       if (format == ast::ImageFormat::kNone) {
         return nullptr;
       }
-      auto* subtype = sem::StorageTexture::SubtypeFor(format, builder_.Types());
-      ast_store_type = builder_.create<sem::AccessControl>(
-          access, builder_.create<sem::StorageTexture>(dim, format, subtype));
+      ast_store_type =
+          builder_.ty.access(access, builder_.ty.storage_texture(dim, format));
     }
   } else {
     Fail() << "unsupported: UniformConstant variable is not a recognized "
@@ -2044,14 +2076,14 @@
   }
 
   // Form the pointer type.
-  auto* result = builder_.create<sem::Pointer>(
-      ast_store_type, ast::StorageClass::kUniformConstant);
+  auto result =
+      builder_.ty.pointer(ast_store_type, ast::StorageClass::kUniformConstant);
   // Remember it for later.
   handle_type_[&var] = result;
   return result;
 }
 
-sem::Type* ParserImpl::GetComponentTypeForFormat(ast::ImageFormat format) {
+typ::Type ParserImpl::GetComponentTypeForFormat(ast::ImageFormat format) {
   switch (format) {
     case ast::ImageFormat::kR8Uint:
     case ast::ImageFormat::kR16Uint:
@@ -2062,7 +2094,7 @@
     case ast::ImageFormat::kRg32Uint:
     case ast::ImageFormat::kRgba16Uint:
     case ast::ImageFormat::kRgba32Uint:
-      return builder_.create<sem::U32>();
+      return builder_.ty.u32();
 
     case ast::ImageFormat::kR8Sint:
     case ast::ImageFormat::kR16Sint:
@@ -2073,7 +2105,7 @@
     case ast::ImageFormat::kRg32Sint:
     case ast::ImageFormat::kRgba16Sint:
     case ast::ImageFormat::kRgba32Sint:
-      return builder_.create<sem::I32>();
+      return builder_.ty.i32();
 
     case ast::ImageFormat::kR8Unorm:
     case ast::ImageFormat::kRg8Unorm:
@@ -2092,7 +2124,7 @@
     case ast::ImageFormat::kRg32Float:
     case ast::ImageFormat::kRgba16Float:
     case ast::ImageFormat::kRgba32Float:
-      return builder_.create<sem::F32>();
+      return builder_.ty.f32();
     default:
       break;
   }
@@ -2100,8 +2132,8 @@
   return nullptr;
 }
 
-sem::Type* ParserImpl::GetTexelTypeForFormat(ast::ImageFormat format) {
-  auto* component_type = GetComponentTypeForFormat(format);
+typ::Type ParserImpl::GetTexelTypeForFormat(ast::ImageFormat format) {
+  auto component_type = GetComponentTypeForFormat(format);
   if (!component_type) {
     return nullptr;
   }
@@ -2132,7 +2164,7 @@
     case ast::ImageFormat::kRg8Uint:
     case ast::ImageFormat::kRg8Unorm:
       // Two channels
-      return builder_.create<sem::Vector>(component_type, 2);
+      return builder_.ty.vec(component_type, 2);
 
     case ast::ImageFormat::kBgra8Unorm:
     case ast::ImageFormat::kBgra8UnormSrgb:
@@ -2149,7 +2181,7 @@
     case ast::ImageFormat::kRgba8Unorm:
     case ast::ImageFormat::kRgba8UnormSrgb:
       // Four channels
-      return builder_.create<sem::Vector>(component_type, 4);
+      return builder_.ty.vec(component_type, 4);
 
     default:
       break;
diff --git a/src/reader/spirv/parser_impl.h b/src/reader/spirv/parser_impl.h
index 5e5e5dd..425e8e5 100644
--- a/src/reader/spirv/parser_impl.h
+++ b/src/reader/spirv/parser_impl.h
@@ -29,6 +29,7 @@
 #include "src/reader/spirv/enum_converter.h"
 #include "src/reader/spirv/namer.h"
 #include "src/reader/spirv/usage.h"
+#include "src/typepair.h"
 
 /// This is the implementation of the SPIR-V parser for Tint.
 
@@ -60,8 +61,22 @@
 
 /// An AST expression with its type.
 struct TypedExpression {
+  /// Constructor
+  TypedExpression();
+
+  /// Copy constructor
+  TypedExpression(const TypedExpression&);
+
+  /// Assignment operator
+  TypedExpression& operator=(const TypedExpression&);
+
+  /// Constructor
+  /// @param type_in the type of the expression
+  /// @param expr_in the expression
+  TypedExpression(typ::Type type_in, ast::Expression* expr_in);
+
   /// The type
-  sem::Type* type = nullptr;
+  typ::Type type;
   /// The expression
   ast::Expression* expr = nullptr;
 };
@@ -140,7 +155,7 @@
   /// after the internal representation of the module has been built.
   /// @param type_id the SPIR-V ID of a type.
   /// @returns a Tint type, or nullptr
-  sem::Type* ConvertType(uint32_t type_id);
+  typ::Type ConvertType(uint32_t type_id);
 
   /// Emits an alias type declaration for the given type, if necessary, and
   /// also updates the mapping of the SPIR-V type ID to the alias type.
@@ -151,8 +166,11 @@
   /// This is a no-op if the parser has already failed.
   /// @param type_id the SPIR-V ID for the type
   /// @param type the type that might get an alias
-  void MaybeGenerateAlias(uint32_t type_id,
-                          const spvtools::opt::analysis::Type* type);
+  /// @param ast_type the ast type that might get an alias
+  /// @returns an alias type or `ast_type` if no alias was created
+  typ::Type MaybeGenerateAlias(uint32_t type_id,
+                               const spvtools::opt::analysis::Type* type,
+                               typ::Type ast_type);
 
   /// @returns the fail stream object
   FailStream& fail_stream() { return fail_stream_; }
@@ -302,7 +320,7 @@
   /// in the error case
   ast::Variable* MakeVariable(uint32_t id,
                               ast::StorageClass sc,
-                              sem::Type* type,
+                              typ::Type type,
                               bool is_const,
                               ast::Expression* constructor,
                               ast::DecorationList decorations);
@@ -315,12 +333,12 @@
   /// Creates an AST expression node for the null value for the given type.
   /// @param type the AST type
   /// @returns a new expression
-  ast::Expression* MakeNullValue(sem::Type* type);
+  ast::Expression* MakeNullValue(typ::Type type);
 
   /// Make a typed expression for the null value for the given type.
   /// @param type the AST type
   /// @returns a new typed expression
-  TypedExpression MakeNullExpression(sem::Type* type);
+  TypedExpression MakeNullExpression(typ::Type type);
 
   /// Converts a given expression to the signedness demanded for an operand
   /// of the given SPIR-V instruction, if required.  If the instruction assumes
@@ -345,7 +363,7 @@
   /// @returns second_operand_expr, or a cast of it
   TypedExpression RectifySecondOperandSignedness(
       const spvtools::opt::Instruction& inst,
-      sem::Type* first_operand_type,
+      typ::Type first_operand_type,
       TypedExpression&& second_operand_expr);
 
   /// Returns the "forced" result type for the given SPIR-V instruction.
@@ -356,8 +374,8 @@
   /// @param inst the SPIR-V instruction
   /// @param first_operand_type the AST type for the first operand.
   /// @returns the forced AST result type, or nullptr if no forcing is required.
-  sem::Type* ForcedResultType(const spvtools::opt::Instruction& inst,
-                              sem::Type* first_operand_type);
+  typ::Type ForcedResultType(const spvtools::opt::Instruction& inst,
+                             typ::Type first_operand_type);
 
   /// Returns a signed integer scalar or vector type matching the shape (scalar,
   /// vector, and component bit width) of another type, which itself is a
@@ -365,7 +383,7 @@
   /// requirement.
   /// @param other the type whose shape must be matched
   /// @returns the signed scalar or vector type
-  sem::Type* GetSignedIntMatchingShape(sem::Type* other);
+  typ::Type GetSignedIntMatchingShape(typ::Type other);
 
   /// Returns a signed integer scalar or vector type matching the shape (scalar,
   /// vector, and component bit width) of another type, which itself is a
@@ -373,7 +391,7 @@
   /// requirement.
   /// @param other the type whose shape must be matched
   /// @returns the unsigned scalar or vector type
-  sem::Type* GetUnsignedIntMatchingShape(sem::Type* other);
+  typ::Type GetUnsignedIntMatchingShape(typ::Type other);
 
   /// Wraps the given expression in an as-cast to the given expression's type,
   /// when the underlying operation produces a forced result type different
@@ -386,10 +404,7 @@
   TypedExpression RectifyForcedResultType(
       TypedExpression expr,
       const spvtools::opt::Instruction& inst,
-      sem::Type* first_operand_type);
-
-  /// @returns the registered boolean type.
-  sem::Type* Bool() const { return bool_type_; }
+      typ::Type first_operand_type);
 
   /// Bookkeeping used for tracking the "position" builtin variable.
   struct BuiltInPositionInfo {
@@ -477,18 +492,18 @@
   /// @param var the OpVariable instruction
   /// @returns the Tint AST type for the poiner-to-{sampler|texture} or null on
   /// error
-  sem::Pointer* GetTypeForHandleVar(const spvtools::opt::Instruction& var);
+  typ::Pointer GetTypeForHandleVar(const spvtools::opt::Instruction& var);
 
   /// Returns the channel component type corresponding to the given image
   /// format.
   /// @param format image texel format
   /// @returns the component type, one of f32, i32, u32
-  sem::Type* GetComponentTypeForFormat(ast::ImageFormat format);
+  typ::Type GetComponentTypeForFormat(ast::ImageFormat format);
 
   /// Returns texel type corresponding to the given image format.
   /// @param format image texel format
   /// @returns the texel format
-  sem::Type* GetTexelTypeForFormat(ast::ImageFormat format);
+  typ::Type GetTexelTypeForFormat(ast::ImageFormat format);
 
   /// Returns the SPIR-V instruction with the given ID, or nullptr.
   /// @param id the SPIR-V result ID
@@ -523,19 +538,19 @@
 
  private:
   /// Converts a specific SPIR-V type to a Tint type. Integer case
-  sem::Type* ConvertType(const spvtools::opt::analysis::Integer* int_ty);
+  typ::Type ConvertType(const spvtools::opt::analysis::Integer* int_ty);
   /// Converts a specific SPIR-V type to a Tint type. Float case
-  sem::Type* ConvertType(const spvtools::opt::analysis::Float* float_ty);
+  typ::Type ConvertType(const spvtools::opt::analysis::Float* float_ty);
   /// Converts a specific SPIR-V type to a Tint type. Vector case
-  sem::Type* ConvertType(const spvtools::opt::analysis::Vector* vec_ty);
+  typ::Type ConvertType(const spvtools::opt::analysis::Vector* vec_ty);
   /// Converts a specific SPIR-V type to a Tint type. Matrix case
-  sem::Type* ConvertType(const spvtools::opt::analysis::Matrix* mat_ty);
+  typ::Type ConvertType(const spvtools::opt::analysis::Matrix* mat_ty);
   /// Converts a specific SPIR-V type to a Tint type. RuntimeArray case
   /// @param rtarr_ty the Tint type
-  sem::Type* ConvertType(const spvtools::opt::analysis::RuntimeArray* rtarr_ty);
+  typ::Type ConvertType(const spvtools::opt::analysis::RuntimeArray* rtarr_ty);
   /// Converts a specific SPIR-V type to a Tint type. Array case
   /// @param arr_ty the Tint type
-  sem::Type* ConvertType(const spvtools::opt::analysis::Array* arr_ty);
+  typ::Type ConvertType(const spvtools::opt::analysis::Array* arr_ty);
   /// Converts a specific SPIR-V type to a Tint type. Struct case.
   /// SPIR-V allows distinct struct type definitions for two OpTypeStruct
   /// that otherwise have the same set of members (and struct and member
@@ -547,15 +562,27 @@
   /// not significant to the optimizer's module representation.
   /// @param type_id the SPIR-V ID for the type.
   /// @param struct_ty the Tint type
-  sem::Type* ConvertType(uint32_t type_id,
-                         const spvtools::opt::analysis::Struct* struct_ty);
+  typ::Type ConvertType(uint32_t type_id,
+                        const spvtools::opt::analysis::Struct* struct_ty);
   /// Converts a specific SPIR-V type to a Tint type. Pointer case
   /// The pointer to gl_PerVertex maps to nullptr, and instead is recorded
   /// in member #builtin_position_.
   /// @param type_id the SPIR-V ID for the type.
   /// @param ptr_ty the Tint type
-  sem::Type* ConvertType(uint32_t type_id,
-                         const spvtools::opt::analysis::Pointer* ptr_ty);
+  typ::Type ConvertType(uint32_t type_id,
+                        const spvtools::opt::analysis::Pointer* ptr_ty);
+
+  /// If `type` is a signed integral, or vector of signed integral,
+  /// returns the unsigned type, otherwise returns `type`.
+  /// @param type the possibly signed type
+  /// @returns the unsigned type
+  typ::Type UnsignedTypeFor(typ::Type type);
+
+  /// If `type` is a unsigned integral, or vector of unsigned integral,
+  /// returns the signed type, otherwise returns `type`.
+  /// @param type the possibly unsigned type
+  /// @returns the signed type
+  typ::Type SignedTypeFor(typ::Type type);
 
   /// Parses the array or runtime-array decorations.
   /// @param spv_type the SPIR-V array or runtime-array type.
@@ -585,9 +612,6 @@
   FailStream fail_stream_;
   spvtools::MessageConsumer message_consumer_;
 
-  // The registered boolean type.
-  sem::Type* bool_type_;
-
   // An object used to store and generate names for SPIR-V objects.
   Namer namer_;
   // An object used to convert SPIR-V enums to Tint enums
@@ -621,14 +645,6 @@
   // "NonSemanticInfo." import is ignored.
   std::unordered_set<uint32_t> ignored_imports_;
 
-  // Maps a SPIR-V type ID to the corresponding Tint type.
-  std::unordered_map<uint32_t, sem::Type*> id_to_type_;
-
-  // Maps an unsigned type corresponding to the given signed type.
-  std::unordered_map<sem::Type*, sem::Type*> signed_type_for_;
-  // Maps an signed type corresponding to the given unsigned type.
-  std::unordered_map<sem::Type*, sem::Type*> unsigned_type_for_;
-
   // Bookkeeping for the gl_Position builtin.
   // In Vulkan SPIR-V, it's the 0 member of the gl_PerVertex structure.
   // But in WGSL we make a module-scope variable:
@@ -646,8 +662,8 @@
   // and Block decoration.
   std::unordered_set<uint32_t> remap_buffer_block_type_;
 
-  // The struct types with only read-only members.
-  std::unordered_set<sem::Type*> read_only_struct_types_;
+  // The ast::Struct type names with only read-only members.
+  std::unordered_set<Symbol> read_only_struct_types_;
 
   // The IDs of scalar spec constants
   std::unordered_set<uint32_t> scalar_spec_constants_;
@@ -672,7 +688,7 @@
   // usages implied by usages of the memory-object-declaration.
   std::unordered_map<const spvtools::opt::Instruction*, Usage> handle_usage_;
   // The inferred pointer type for the given handle variable.
-  std::unordered_map<const spvtools::opt::Instruction*, sem::Pointer*>
+  std::unordered_map<const spvtools::opt::Instruction*, typ::Pointer>
       handle_type_;
 
   /// Maps the SPIR-V ID of a module-scope builtin variable that should be
diff --git a/src/reader/spirv/parser_impl_convert_type_test.cc b/src/reader/spirv/parser_impl_convert_type_test.cc
index 5915beb..088cbe4 100644
--- a/src/reader/spirv/parser_impl_convert_type_test.cc
+++ b/src/reader/spirv/parser_impl_convert_type_test.cc
@@ -26,14 +26,14 @@
 TEST_F(SpvParserTest, ConvertType_PreservesExistingFailure) {
   auto p = parser(std::vector<uint32_t>{});
   p->Fail() << "boing";
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   EXPECT_EQ(type, nullptr);
   EXPECT_THAT(p->error(), Eq("boing"));
 }
 
 TEST_F(SpvParserTest, ConvertType_RequiresInternalRepresntation) {
   auto p = parser(std::vector<uint32_t>{});
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   EXPECT_EQ(type, nullptr);
   EXPECT_THAT(
       p->error(),
@@ -44,7 +44,7 @@
   auto p = parser(test::Assemble("%1 = OpExtInstImport \"GLSL.std.450\""));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   EXPECT_EQ(type, nullptr);
   EXPECT_EQ(nullptr, type);
   EXPECT_THAT(p->error(), Eq("ID is not a SPIR-V type: 10"));
@@ -54,7 +54,7 @@
   auto p = parser(test::Assemble("%1 = OpExtInstImport \"GLSL.std.450\""));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(1);
+  auto type = p->ConvertType(1);
   EXPECT_EQ(nullptr, type);
   EXPECT_THAT(p->error(), Eq("ID is not a SPIR-V type: 1"));
 }
@@ -64,7 +64,7 @@
   auto p = parser(test::Assemble("%70 = OpTypePipe WriteOnly"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(70);
+  auto type = p->ConvertType(70);
   EXPECT_EQ(nullptr, type);
   EXPECT_THAT(p->error(),
               Eq("unknown SPIR-V type with ID 70: %70 = OpTypePipe WriteOnly"));
@@ -74,7 +74,7 @@
   auto p = parser(test::Assemble("%1 = OpTypeVoid"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(1);
+  auto type = p->ConvertType(1);
   EXPECT_TRUE(type->Is<sem::Void>());
   EXPECT_TRUE(p->error().empty());
 }
@@ -83,7 +83,7 @@
   auto p = parser(test::Assemble("%100 = OpTypeBool"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(100);
+  auto type = p->ConvertType(100);
   EXPECT_TRUE(type->Is<sem::Bool>());
   EXPECT_TRUE(p->error().empty());
 }
@@ -92,7 +92,7 @@
   auto p = parser(test::Assemble("%2 = OpTypeInt 32 1"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(2);
+  auto type = p->ConvertType(2);
   EXPECT_TRUE(type->Is<sem::I32>());
   EXPECT_TRUE(p->error().empty());
 }
@@ -101,7 +101,7 @@
   auto p = parser(test::Assemble("%3 = OpTypeInt 32 0"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(3);
+  auto type = p->ConvertType(3);
   EXPECT_TRUE(type->Is<sem::U32>());
   EXPECT_TRUE(p->error().empty());
 }
@@ -110,7 +110,7 @@
   auto p = parser(test::Assemble("%4 = OpTypeFloat 32"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(4);
+  auto type = p->ConvertType(4);
   EXPECT_TRUE(type->Is<sem::F32>());
   EXPECT_TRUE(p->error().empty());
 }
@@ -119,7 +119,7 @@
   auto p = parser(test::Assemble("%5 = OpTypeInt 17 1"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(5);
+  auto type = p->ConvertType(5);
   EXPECT_EQ(type, nullptr);
   EXPECT_THAT(p->error(), Eq("unhandled integer width: 17"));
 }
@@ -128,7 +128,7 @@
   auto p = parser(test::Assemble("%6 = OpTypeFloat 19"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(6);
+  auto type = p->ConvertType(6);
   EXPECT_EQ(type, nullptr);
   EXPECT_THAT(p->error(), Eq("unhandled float width: 19"));
 }
@@ -140,7 +140,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(20);
+  auto type = p->ConvertType(20);
   EXPECT_EQ(type, nullptr);
   EXPECT_THAT(p->error(), Eq("unknown SPIR-V type: 5"));
 }
@@ -154,17 +154,17 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* v2xf32 = p->ConvertType(20);
+  auto v2xf32 = p->ConvertType(20);
   EXPECT_TRUE(v2xf32->Is<sem::Vector>());
   EXPECT_TRUE(v2xf32->As<sem::Vector>()->type()->Is<sem::F32>());
   EXPECT_EQ(v2xf32->As<sem::Vector>()->size(), 2u);
 
-  auto* v3xf32 = p->ConvertType(30);
+  auto v3xf32 = p->ConvertType(30);
   EXPECT_TRUE(v3xf32->Is<sem::Vector>());
   EXPECT_TRUE(v3xf32->As<sem::Vector>()->type()->Is<sem::F32>());
   EXPECT_EQ(v3xf32->As<sem::Vector>()->size(), 3u);
 
-  auto* v4xf32 = p->ConvertType(40);
+  auto v4xf32 = p->ConvertType(40);
   EXPECT_TRUE(v4xf32->Is<sem::Vector>());
   EXPECT_TRUE(v4xf32->As<sem::Vector>()->type()->Is<sem::F32>());
   EXPECT_EQ(v4xf32->As<sem::Vector>()->size(), 4u);
@@ -181,17 +181,17 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* v2xi32 = p->ConvertType(20);
+  auto v2xi32 = p->ConvertType(20);
   EXPECT_TRUE(v2xi32->Is<sem::Vector>());
   EXPECT_TRUE(v2xi32->As<sem::Vector>()->type()->Is<sem::I32>());
   EXPECT_EQ(v2xi32->As<sem::Vector>()->size(), 2u);
 
-  auto* v3xi32 = p->ConvertType(30);
+  auto v3xi32 = p->ConvertType(30);
   EXPECT_TRUE(v3xi32->Is<sem::Vector>());
   EXPECT_TRUE(v3xi32->As<sem::Vector>()->type()->Is<sem::I32>());
   EXPECT_EQ(v3xi32->As<sem::Vector>()->size(), 3u);
 
-  auto* v4xi32 = p->ConvertType(40);
+  auto v4xi32 = p->ConvertType(40);
   EXPECT_TRUE(v4xi32->Is<sem::Vector>());
   EXPECT_TRUE(v4xi32->As<sem::Vector>()->type()->Is<sem::I32>());
   EXPECT_EQ(v4xi32->As<sem::Vector>()->size(), 4u);
@@ -208,17 +208,17 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* v2xu32 = p->ConvertType(20);
+  auto v2xu32 = p->ConvertType(20);
   EXPECT_TRUE(v2xu32->Is<sem::Vector>());
   EXPECT_TRUE(v2xu32->As<sem::Vector>()->type()->Is<sem::U32>());
   EXPECT_EQ(v2xu32->As<sem::Vector>()->size(), 2u);
 
-  auto* v3xu32 = p->ConvertType(30);
+  auto v3xu32 = p->ConvertType(30);
   EXPECT_TRUE(v3xu32->Is<sem::Vector>());
   EXPECT_TRUE(v3xu32->As<sem::Vector>()->type()->Is<sem::U32>());
   EXPECT_EQ(v3xu32->As<sem::Vector>()->size(), 3u);
 
-  auto* v4xu32 = p->ConvertType(40);
+  auto v4xu32 = p->ConvertType(40);
   EXPECT_TRUE(v4xu32->Is<sem::Vector>());
   EXPECT_TRUE(v4xu32->As<sem::Vector>()->type()->Is<sem::U32>());
   EXPECT_EQ(v4xu32->As<sem::Vector>()->size(), 4u);
@@ -234,7 +234,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(20);
+  auto type = p->ConvertType(20);
   EXPECT_EQ(type, nullptr);
   EXPECT_THAT(p->error(), Eq("unknown SPIR-V type: 5"));
 }
@@ -260,55 +260,55 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* m22 = p->ConvertType(22);
+  auto m22 = p->ConvertType(22);
   EXPECT_TRUE(m22->Is<sem::Matrix>());
   EXPECT_TRUE(m22->As<sem::Matrix>()->type()->Is<sem::F32>());
   EXPECT_EQ(m22->As<sem::Matrix>()->rows(), 2u);
   EXPECT_EQ(m22->As<sem::Matrix>()->columns(), 2u);
 
-  auto* m23 = p->ConvertType(23);
+  auto m23 = p->ConvertType(23);
   EXPECT_TRUE(m23->Is<sem::Matrix>());
   EXPECT_TRUE(m23->As<sem::Matrix>()->type()->Is<sem::F32>());
   EXPECT_EQ(m23->As<sem::Matrix>()->rows(), 2u);
   EXPECT_EQ(m23->As<sem::Matrix>()->columns(), 3u);
 
-  auto* m24 = p->ConvertType(24);
+  auto m24 = p->ConvertType(24);
   EXPECT_TRUE(m24->Is<sem::Matrix>());
   EXPECT_TRUE(m24->As<sem::Matrix>()->type()->Is<sem::F32>());
   EXPECT_EQ(m24->As<sem::Matrix>()->rows(), 2u);
   EXPECT_EQ(m24->As<sem::Matrix>()->columns(), 4u);
 
-  auto* m32 = p->ConvertType(32);
+  auto m32 = p->ConvertType(32);
   EXPECT_TRUE(m32->Is<sem::Matrix>());
   EXPECT_TRUE(m32->As<sem::Matrix>()->type()->Is<sem::F32>());
   EXPECT_EQ(m32->As<sem::Matrix>()->rows(), 3u);
   EXPECT_EQ(m32->As<sem::Matrix>()->columns(), 2u);
 
-  auto* m33 = p->ConvertType(33);
+  auto m33 = p->ConvertType(33);
   EXPECT_TRUE(m33->Is<sem::Matrix>());
   EXPECT_TRUE(m33->As<sem::Matrix>()->type()->Is<sem::F32>());
   EXPECT_EQ(m33->As<sem::Matrix>()->rows(), 3u);
   EXPECT_EQ(m33->As<sem::Matrix>()->columns(), 3u);
 
-  auto* m34 = p->ConvertType(34);
+  auto m34 = p->ConvertType(34);
   EXPECT_TRUE(m34->Is<sem::Matrix>());
   EXPECT_TRUE(m34->As<sem::Matrix>()->type()->Is<sem::F32>());
   EXPECT_EQ(m34->As<sem::Matrix>()->rows(), 3u);
   EXPECT_EQ(m34->As<sem::Matrix>()->columns(), 4u);
 
-  auto* m42 = p->ConvertType(42);
+  auto m42 = p->ConvertType(42);
   EXPECT_TRUE(m42->Is<sem::Matrix>());
   EXPECT_TRUE(m42->As<sem::Matrix>()->type()->Is<sem::F32>());
   EXPECT_EQ(m42->As<sem::Matrix>()->rows(), 4u);
   EXPECT_EQ(m42->As<sem::Matrix>()->columns(), 2u);
 
-  auto* m43 = p->ConvertType(43);
+  auto m43 = p->ConvertType(43);
   EXPECT_TRUE(m43->Is<sem::Matrix>());
   EXPECT_TRUE(m43->As<sem::Matrix>()->type()->Is<sem::F32>());
   EXPECT_EQ(m43->As<sem::Matrix>()->rows(), 4u);
   EXPECT_EQ(m43->As<sem::Matrix>()->columns(), 3u);
 
-  auto* m44 = p->ConvertType(44);
+  auto m44 = p->ConvertType(44);
   EXPECT_TRUE(m44->Is<sem::Matrix>());
   EXPECT_TRUE(m44->As<sem::Matrix>()->type()->Is<sem::F32>());
   EXPECT_EQ(m44->As<sem::Matrix>()->rows(), 4u);
@@ -324,10 +324,10 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   ASSERT_NE(type, nullptr);
-  EXPECT_TRUE(type->Is<sem::ArrayType>());
-  auto* arr_type = type->As<sem::ArrayType>();
+  EXPECT_TRUE(type->UnwrapAliasIfNeeded()->Is<sem::ArrayType>());
+  auto* arr_type = type->UnwrapAliasIfNeeded()->As<sem::ArrayType>();
   EXPECT_TRUE(arr_type->IsRuntimeArray());
   ASSERT_NE(arr_type, nullptr);
   EXPECT_EQ(arr_type->size(), 0u);
@@ -345,7 +345,7 @@
     %10 = OpTypeRuntimeArray %uint
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   EXPECT_EQ(type, nullptr);
   EXPECT_THAT(
       p->error(),
@@ -359,9 +359,9 @@
     %10 = OpTypeRuntimeArray %uint
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   ASSERT_NE(type, nullptr);
-  auto* arr_type = type->As<sem::ArrayType>();
+  auto* arr_type = type->UnwrapAliasIfNeeded()->As<sem::ArrayType>();
   EXPECT_TRUE(arr_type->IsRuntimeArray());
   ASSERT_NE(arr_type, nullptr);
   ASSERT_EQ(arr_type->decorations().size(), 1u);
@@ -378,7 +378,7 @@
     %10 = OpTypeRuntimeArray %uint
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   EXPECT_EQ(type, nullptr);
   EXPECT_THAT(p->error(),
               Eq("invalid array type ID 10: ArrayStride can't be 0"));
@@ -393,7 +393,7 @@
     %10 = OpTypeRuntimeArray %uint
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   EXPECT_EQ(type, nullptr);
   EXPECT_THAT(p->error(),
               Eq("invalid array type ID 10: multiple ArrayStride decorations"));
@@ -407,7 +407,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   ASSERT_NE(type, nullptr);
   EXPECT_TRUE(type->Is<sem::ArrayType>());
   auto* arr_type = type->As<sem::ArrayType>();
@@ -430,7 +430,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   ASSERT_EQ(type, nullptr);
   EXPECT_THAT(p->error(),
               Eq("Array type 10 length is a specialization constant"));
@@ -445,7 +445,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   ASSERT_EQ(type, nullptr);
   EXPECT_THAT(p->error(),
               Eq("Array type 10 length is a specialization constant"));
@@ -463,7 +463,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   ASSERT_EQ(type, nullptr);
   // TODO(dneto): Right now it's rejected earlier in the flow because
   // we can't even utter the uint64 type.
@@ -478,7 +478,7 @@
     %10 = OpTypeArray %uint %uint_5
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   EXPECT_EQ(type, nullptr);
   EXPECT_THAT(
       p->error(),
@@ -494,10 +494,10 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   ASSERT_NE(type, nullptr);
-  EXPECT_TRUE(type->Is<sem::ArrayType>());
-  auto* arr_type = type->As<sem::ArrayType>();
+  EXPECT_TRUE(type->UnwrapAliasIfNeeded()->Is<sem::ArrayType>());
+  auto* arr_type = type->UnwrapAliasIfNeeded()->As<sem::ArrayType>();
   ASSERT_NE(arr_type, nullptr);
 
   ASSERT_EQ(arr_type->decorations().size(), 1u);
@@ -517,7 +517,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   ASSERT_EQ(type, nullptr);
   EXPECT_THAT(p->error(),
               Eq("invalid array type ID 10: ArrayStride can't be 0"));
@@ -533,7 +533,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   ASSERT_EQ(type, nullptr);
   EXPECT_THAT(p->error(),
               Eq("invalid array type ID 10: multiple ArrayStride decorations"));
@@ -548,7 +548,7 @@
   EXPECT_TRUE(p->BuildInternalModule());
   EXPECT_TRUE(p->RegisterUserAndStructMemberNames());
 
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   ASSERT_NE(type, nullptr);
   EXPECT_TRUE(type->Is<sem::StructType>());
 
@@ -569,7 +569,7 @@
   EXPECT_TRUE(p->BuildInternalModule());
   EXPECT_TRUE(p->RegisterUserAndStructMemberNames());
 
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   ASSERT_NE(type, nullptr);
   EXPECT_TRUE(type->Is<sem::StructType>());
 
@@ -594,7 +594,7 @@
   EXPECT_TRUE(p->BuildInternalModule());
   EXPECT_TRUE(p->RegisterUserAndStructMemberNames());
 
-  auto* type = p->ConvertType(10);
+  auto type = p->ConvertType(10);
   ASSERT_NE(type, nullptr);
   EXPECT_TRUE(type->Is<sem::StructType>());
 
@@ -621,7 +621,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule()) << p->error();
 
-  auto* type = p->ConvertType(3);
+  auto type = p->ConvertType(3);
   EXPECT_EQ(type, nullptr);
   EXPECT_THAT(p->error(),
               Eq("SPIR-V pointer type with ID 3 has invalid pointee type 42"));
@@ -644,7 +644,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(3);
+  auto type = p->ConvertType(3);
   EXPECT_TRUE(type->Is<sem::Pointer>());
   auto* ptr_ty = type->As<sem::Pointer>();
   EXPECT_NE(ptr_ty, nullptr);
@@ -660,7 +660,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(3);
+  auto type = p->ConvertType(3);
   EXPECT_TRUE(type->Is<sem::Pointer>());
   auto* ptr_ty = type->As<sem::Pointer>();
   EXPECT_NE(ptr_ty, nullptr);
@@ -676,7 +676,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(3);
+  auto type = p->ConvertType(3);
   EXPECT_TRUE(type->Is<sem::Pointer>());
   auto* ptr_ty = type->As<sem::Pointer>();
   EXPECT_NE(ptr_ty, nullptr);
@@ -692,7 +692,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(3);
+  auto type = p->ConvertType(3);
   EXPECT_TRUE(type->Is<sem::Pointer>());
   auto* ptr_ty = type->As<sem::Pointer>();
   EXPECT_NE(ptr_ty, nullptr);
@@ -708,7 +708,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(3);
+  auto type = p->ConvertType(3);
   EXPECT_TRUE(type->Is<sem::Pointer>());
   auto* ptr_ty = type->As<sem::Pointer>();
   EXPECT_NE(ptr_ty, nullptr);
@@ -724,7 +724,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(3);
+  auto type = p->ConvertType(3);
   EXPECT_TRUE(type->Is<sem::Pointer>());
   auto* ptr_ty = type->As<sem::Pointer>();
   EXPECT_NE(ptr_ty, nullptr);
@@ -740,7 +740,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(3);
+  auto type = p->ConvertType(3);
   EXPECT_TRUE(type->Is<sem::Pointer>());
   auto* ptr_ty = type->As<sem::Pointer>();
   EXPECT_NE(ptr_ty, nullptr);
@@ -756,7 +756,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(3);
+  auto type = p->ConvertType(3);
   EXPECT_TRUE(type->Is<sem::Pointer>());
   auto* ptr_ty = type->As<sem::Pointer>();
   EXPECT_NE(ptr_ty, nullptr);
@@ -772,7 +772,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(3);
+  auto type = p->ConvertType(3);
   EXPECT_TRUE(type->Is<sem::Pointer>());
   auto* ptr_ty = type->As<sem::Pointer>();
   EXPECT_NE(ptr_ty, nullptr);
@@ -790,7 +790,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(3);
+  auto type = p->ConvertType(3);
   EXPECT_NE(type, nullptr);
   EXPECT_TRUE(type->Is<sem::Pointer>());
 
@@ -814,7 +814,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(1);
+  auto type = p->ConvertType(1);
   EXPECT_TRUE(type->Is<sem::Void>());
   EXPECT_TRUE(p->error().empty());
 }
@@ -827,7 +827,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(1);
+  auto type = p->ConvertType(1);
   EXPECT_TRUE(type->Is<sem::Void>());
   EXPECT_TRUE(p->error().empty());
 }
@@ -840,7 +840,7 @@
   )"));
   EXPECT_TRUE(p->BuildInternalModule());
 
-  auto* type = p->ConvertType(1);
+  auto type = p->ConvertType(1);
   EXPECT_TRUE(type->Is<sem::Void>());
   EXPECT_TRUE(p->error().empty());
 }
diff --git a/src/typepair.h b/src/typepair.h
index 1d26bf5..4fe005a 100644
--- a/src/typepair.h
+++ b/src/typepair.h
@@ -20,6 +20,8 @@
 #define SRC_TYPEPAIR_H_
 
 #include <cstddef>
+#include <type_traits>
+#include <utility>
 
 // X11 likes to #define Bool leading to confusing error messages.
 // If its defined, undefine it.
@@ -45,6 +47,7 @@
 class SampledTexture;
 class StorageTexture;
 class Struct;
+class Texture;
 class Type;
 class U32;
 class Vector;
@@ -67,6 +70,7 @@
 class SampledTexture;
 class StorageTexture;
 class StructType;
+class Texture;
 class Type;
 class U32;
 class Vector;
@@ -122,6 +126,11 @@
 ///   will switch to returning the ast::Type pointer.
 template <typename AST, typename SEM>
 struct TypePair {
+  /// Alias of the `AST` template type parameter
+  using AST_TYPE = AST;
+  /// Alias of the `SEM` template type parameter
+  using SEM_TYPE = SEM;
+
   /// The ast::Type pointer
   AST const* ast = nullptr;
   /// The sem::Type pointer
@@ -246,10 +255,44 @@
 using SampledTexture = TypePair<ast::SampledTexture, sem::SampledTexture>;
 using StorageTexture = TypePair<ast::StorageTexture, sem::StorageTexture>;
 using Struct = TypePair<ast::Struct, sem::StructType>;
+using Texture = TypePair<ast::Texture, sem::Texture>;
 using U32 = TypePair<ast::U32, sem::U32>;
 using Vector = TypePair<ast::Vector, sem::Vector>;
 using Void = TypePair<ast::Void, sem::Void>;
 
+// Helpers
+
+/// Makes a type pair, deducing the return type from input args
+/// @parm ast the ast node
+/// @param sem the sem node
+/// @returns a type pair
+template <typename AST, typename SEM>
+inline auto MakeTypePair(AST* ast, SEM* sem) {
+  return TypePair<AST, SEM>{ast, sem};
+}
+
+/// Performs an As operation on the `ast` and `sem` members of the input type
+/// pair, deducing the mapped type from typ::* to ast::* and sem::*
+/// respectively.
+/// @param tp the type pair to call As on
+/// @returns a new type pair after As has been called on each of `sem` and `ast`
+template <typename TargetTYP, typename AST, typename SEM>
+auto As(TypePair<AST, SEM> tp)
+    -> TypePair<typename TargetTYP::AST_TYPE, typename TargetTYP::SEM_TYPE> {
+  return MakeTypePair(
+      tp.ast ? tp.ast->template As<typename TargetTYP::AST_TYPE>() : nullptr,
+      tp.sem ? tp.sem->template As<typename TargetTYP::SEM_TYPE>() : nullptr);
+}
+
+/// Invokes the `type()` member function on each of `ast` and `sem` of the input
+/// type pair
+/// @param tp the type pair
+/// @returns a type pair with the result of calling `type()` on `ast` and `sem`
+template <typename AST, typename SEM>
+TypePair<AST, SEM> Call_type(TypePair<AST, SEM> tp) {
+  return MakeTypePair(tp.ast->type(), tp.sem->type());
+}
+
 }  // namespace typ
 
 }  // namespace tint