wgsl parser: use new TypesBuilder factory functions, and set Source for ast::Type nodes

* ProgramBuilder: added a bunch of overloads that take Source

* Added MultiTokenSource RAII helper to build source ranges for
multi-token types

* Added comparison operators to Source::Range and Source::Location to
make it easier to write tests to compare Source ranges

* Moved CombineSourceRange from resolver.cc to a static function in
Source named Source::Combine()

* Added Source tests for all ast type nodes returned by the wgsl parser

Bug: tint:724
Change-Id: I6fb6211a3c42c14693df8746af6a30f5aa56f2af
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/48963
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Antonio Maiorano <amaiorano@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/program_builder.h b/src/program_builder.h
index 1b5da81..94f410c 100644
--- a/src/program_builder.h
+++ b/src/program_builder.h
@@ -28,6 +28,7 @@
 #include "src/ast/call_expression.h"
 #include "src/ast/case_statement.h"
 #include "src/ast/depth_texture.h"
+#include "src/ast/external_texture.h"
 #include "src/ast/f32.h"
 #include "src/ast/float_literal.h"
 #include "src/ast/i32.h"
@@ -62,6 +63,7 @@
 #include "src/sem/array_type.h"
 #include "src/sem/bool_type.h"
 #include "src/sem/depth_texture_type.h"
+#include "src/sem/external_texture_type.h"
 #include "src/sem/f32_type.h"
 #include "src/sem/i32_type.h"
 #include "src/sem/matrix_type.h"
@@ -352,26 +354,56 @@
       return {builder->create<ast::Bool>(), builder->create<sem::Bool>()};
     }
 
+    /// @param source the Source of the node
+    /// @returns a boolean type
+    typ::Bool bool_(const Source& source) const {
+      return {builder->create<ast::Bool>(source), builder->create<sem::Bool>()};
+    }
+
     /// @returns a f32 type
     typ::F32 f32() const {
       return {builder->create<ast::F32>(), builder->create<sem::F32>()};
     }
 
+    /// @param source the Source of the node
+    /// @returns a f32 type
+    typ::F32 f32(const Source& source) const {
+      return {builder->create<ast::F32>(source), builder->create<sem::F32>()};
+    }
+
     /// @returns a i32 type
     typ::I32 i32() const {
       return {builder->create<ast::I32>(), builder->create<sem::I32>()};
     }
 
+    /// @param source the Source of the node
+    /// @returns a i32 type
+    typ::I32 i32(const Source& source) const {
+      return {builder->create<ast::I32>(source), builder->create<sem::I32>()};
+    }
+
     /// @returns a u32 type
     typ::U32 u32() const {
       return {builder->create<ast::U32>(), builder->create<sem::U32>()};
     }
 
+    /// @param source the Source of the node
+    /// @returns a u32 type
+    typ::U32 u32(const Source& source) const {
+      return {builder->create<ast::U32>(source), builder->create<sem::U32>()};
+    }
+
     /// @returns a void type
     typ::Void void_() const {
       return {builder->create<ast::Void>(), builder->create<sem::Void>()};
     }
 
+    /// @param source the Source of the node
+    /// @returns a void type
+    typ::Void void_(const Source& source) const {
+      return {builder->create<ast::Void>(source), builder->create<sem::Void>()};
+    }
+
     /// @param type vector subtype
     /// @param n vector width in elements
     /// @return the tint AST type for a `n`-element vector of `type`.
@@ -380,6 +412,15 @@
               builder->create<sem::Vector>(type, n)};
     }
 
+    /// @param source the Source of the node
+    /// @param type vector subtype
+    /// @param n vector width in elements
+    /// @return the tint AST type for a `n`-element vector of `type`.
+    typ::Vector vec(const Source& source, typ::Type type, uint32_t n) const {
+      return {builder->create<ast::Vector>(source, type, n),
+              builder->create<sem::Vector>(type, n)};
+    }
+
     /// @param type vector subtype
     /// @return the tint AST type for a 2-element vector of `type`.
     typ::Vector vec2(typ::Type type) const { return vec(type, 2u); }
@@ -426,6 +467,19 @@
               builder->create<sem::Matrix>(type, rows, columns)};
     }
 
+    /// @param source the Source of the node
+    /// @param type matrix subtype
+    /// @param columns number of columns for the matrix
+    /// @param rows number of rows for the matrix
+    /// @return the tint AST type for a matrix of `type`
+    typ::Matrix mat(const Source& source,
+                    typ::Type type,
+                    uint32_t columns,
+                    uint32_t rows) const {
+      return {builder->create<ast::Matrix>(source, type, rows, columns),
+              builder->create<sem::Matrix>(type, rows, columns)};
+    }
+
     /// @param type matrix subtype
     /// @return the tint AST type for a 2x3 matrix of `type`.
     typ::Matrix mat2x2(typ::Type type) const {
@@ -560,7 +614,21 @@
                      ast::DecorationList decos = {}) const {
       subtype = MaybeCreateTypename(subtype);
       return {builder->create<ast::Array>(subtype, n, decos),
-              builder->create<sem::ArrayType>(subtype, n, decos)};
+              builder->create<sem::ArrayType>(subtype, n, std::move(decos))};
+    }
+
+    /// @param source the Source of the node
+    /// @param subtype the array element type
+    /// @param n the array size. 0 represents a runtime-array
+    /// @param decos the optional decorations for the array
+    /// @return the tint AST type for a array of size `n` of type `T`
+    typ::Array array(const Source& source,
+                     typ::Type subtype,
+                     uint32_t n = 0,
+                     ast::DecorationList decos = {}) const {
+      subtype = MaybeCreateTypename(subtype);
+      return {builder->create<ast::Array>(source, subtype, n, decos),
+              builder->create<sem::ArrayType>(subtype, n, std::move(decos))};
     }
 
     /// @param subtype the array element type
@@ -600,6 +668,21 @@
       };
     }
 
+    /// Creates an alias type
+    /// @param source the Source of the node
+    /// @param name the alias name
+    /// @param type the alias type
+    /// @returns the alias pointer
+    template <typename NAME>
+    typ::Alias alias(const Source& source, NAME&& name, typ::Type type) const {
+      type = MaybeCreateTypename(type);
+      auto sym = builder->Sym(std::forward<NAME>(name));
+      return {
+          builder->create<ast::Alias>(source, sym, type),
+          builder->create<sem::Alias>(sym, type),
+      };
+    }
+
     /// Creates an access control qualifier type
     /// @param access the access control
     /// @param type the inner type
@@ -611,6 +694,19 @@
               builder->create<sem::AccessControl>(access, type)};
     }
 
+    /// Creates an access control qualifier type
+    /// @param source the Source of the node
+    /// @param access the access control
+    /// @param type the inner type
+    /// @returns the access control qualifier type
+    typ::AccessControl access(const Source& source,
+                              ast::AccessControl::Access access,
+                              typ::Type type) const {
+      type = MaybeCreateTypename(type);
+      return {builder->create<ast::AccessControl>(source, access, type),
+              builder->create<sem::AccessControl>(access, type)};
+    }
+
     /// @param type the type of the pointer
     /// @param storage_class the storage class of the pointer
     /// @return the pointer to `type` with the given ast::StorageClass
@@ -621,6 +717,18 @@
               builder->create<sem::Pointer>(type, storage_class)};
     }
 
+    /// @param source the Source of the node
+    /// @param type the type of the pointer
+    /// @param storage_class the storage class of the pointer
+    /// @return the pointer to `type` with the given ast::StorageClass
+    typ::Pointer pointer(const Source& source,
+                         typ::Type type,
+                         ast::StorageClass storage_class) const {
+      type = MaybeCreateTypename(type);
+      return {builder->create<ast::Pointer>(source, type, storage_class),
+              builder->create<sem::Pointer>(type, storage_class)};
+    }
+
     /// @param storage_class the storage class of the pointer
     /// @return the pointer to type `T` with the given ast::StorageClass.
     template <typename T>
@@ -641,6 +749,14 @@
               builder->create<sem::Sampler>(kind)};
     }
 
+    /// @param source the Source of the node
+    /// @param kind the kind of sampler
+    /// @returns the sampler
+    typ::Sampler sampler(const Source& source, ast::SamplerKind kind) const {
+      return {builder->create<ast::Sampler>(source, kind),
+              builder->create<sem::Sampler>(kind)};
+    }
+
     /// @param dims the dimensionality of the texture
     /// @returns the depth texture
     typ::DepthTexture depth_texture(ast::TextureDimension dims) const {
@@ -648,6 +764,15 @@
               builder->create<sem::DepthTexture>(dims)};
     }
 
+    /// @param source the Source of the node
+    /// @param dims the dimensionality of the texture
+    /// @returns the depth texture
+    typ::DepthTexture depth_texture(const Source& source,
+                                    ast::TextureDimension dims) const {
+      return {builder->create<ast::DepthTexture>(source, dims),
+              builder->create<sem::DepthTexture>(dims)};
+    }
+
     /// @param dims the dimensionality of the texture
     /// @param subtype the texture subtype.
     /// @returns the sampled texture
@@ -657,6 +782,17 @@
               builder->create<sem::SampledTexture>(dims, subtype)};
     }
 
+    /// @param source the Source of the node
+    /// @param dims the dimensionality of the texture
+    /// @param subtype the texture subtype.
+    /// @returns the sampled texture
+    typ::SampledTexture sampled_texture(const Source& source,
+                                        ast::TextureDimension dims,
+                                        typ::Type subtype) const {
+      return {builder->create<ast::SampledTexture>(source, dims, subtype),
+              builder->create<sem::SampledTexture>(dims, subtype)};
+    }
+
     /// @param dims the dimensionality of the texture
     /// @param subtype the texture subtype.
     /// @returns the multisampled texture
@@ -666,6 +802,17 @@
               builder->create<sem::MultisampledTexture>(dims, subtype)};
     }
 
+    /// @param source the Source of the node
+    /// @param dims the dimensionality of the texture
+    /// @param subtype the texture subtype.
+    /// @returns the multisampled texture
+    typ::MultisampledTexture multisampled_texture(const Source& source,
+                                                  ast::TextureDimension dims,
+                                                  typ::Type subtype) const {
+      return {builder->create<ast::MultisampledTexture>(source, dims, subtype),
+              builder->create<sem::MultisampledTexture>(dims, subtype)};
+    }
+
     /// @param dims the dimensionality of the texture
     /// @param format the image format of the texture
     /// @returns the storage texture
@@ -678,6 +825,28 @@
               builder->create<sem::StorageTexture>(dims, format, sem_subtype)};
     }
 
+    /// @param source the Source of the node
+    /// @param dims the dimensionality of the texture
+    /// @param format the image format of the texture
+    /// @returns the storage texture
+    typ::StorageTexture storage_texture(const Source& source,
+                                        ast::TextureDimension dims,
+                                        ast::ImageFormat format) const {
+      auto* ast_subtype = ast::StorageTexture::SubtypeFor(format, *builder);
+      auto* sem_subtype =
+          sem::StorageTexture::SubtypeFor(format, builder->Types());
+      return {builder->create<ast::StorageTexture>(source, dims, format,
+                                                   ast_subtype),
+              builder->create<sem::StorageTexture>(dims, format, sem_subtype)};
+    }
+
+    /// @param source the Source of the node
+    /// @returns the external texture
+    typ::ExternalTexture external_texture(const Source& source) const {
+      return {builder->create<ast::ExternalTexture>(source),
+              builder->create<sem::ExternalTexture>()};
+    }
+
     /// If ty is a ast::Struct or ast::Alias, the returned type is an
     /// ast::TypeName of the given type's name, otherwise  type is returned.
     /// @param type the type
diff --git a/src/reader/wgsl/parser_impl.cc b/src/reader/wgsl/parser_impl.cc
index 7536cc4..c44cd2d 100644
--- a/src/reader/wgsl/parser_impl.cc
+++ b/src/reader/wgsl/parser_impl.cc
@@ -177,9 +177,38 @@
     return 0;
   }
 };
-
 }  // namespace
 
+/// RAII helper that combines a Source on construction with the last token's
+/// source when implicitly converted to `Source`.
+class ParserImpl::MultiTokenSource {
+ public:
+  /// Constructor that starts with Source at the current peek position
+  /// @param parser the parser
+  explicit MultiTokenSource(ParserImpl* parser)
+      : MultiTokenSource(parser, parser->peek().source().Begin()) {}
+
+  /// Constructor that starts with the input `start` Source
+  /// @param parser the parser
+  /// @param start the start source of the range
+  MultiTokenSource(ParserImpl* parser, const Source& start)
+      : parser_(parser), start_(start) {}
+
+  /// Implicit conversion to Source that returns the combined source from start
+  /// to the current last token's source.
+  operator Source() const {
+    Source end = parser_->last_token().source().End();
+    if (end < start_) {
+      end = start_;
+    }
+    return Source::Combine(start_, end);
+  }
+
+ private:
+  ParserImpl* parser_;
+  Source start_;
+};
+
 ParserImpl::TypedIdentifier::TypedIdentifier() = default;
 
 ParserImpl::TypedIdentifier::TypedIdentifier(const TypedIdentifier&) = default;
@@ -266,15 +295,16 @@
   if (!token_queue_.empty()) {
     auto t = token_queue_.front();
     token_queue_.pop_front();
-    return t;
+    last_token_ = t;
+    return last_token_;
   }
-  return lexer_->next();
+  last_token_ = lexer_->next();
+  return last_token_;
 }
 
 Token ParserImpl::peek(size_t idx) {
   while (token_queue_.size() < (idx + 1))
     token_queue_.push_back(lexer_->next());
-
   return token_queue_[idx];
 }
 
@@ -282,6 +312,10 @@
   return peek(0);
 }
 
+Token ParserImpl::last_token() const {
+  return last_token_;
+}
+
 void ParserImpl::register_constructed(const std::string& name,
                                       sem::Type* type) {
   registered_constructs_[name] = type;
@@ -560,6 +594,8 @@
   if (type.matched)
     return type.value;
 
+  auto source_range = make_source_range();
+
   auto dim = sampled_texture_type();
   if (dim.matched) {
     const char* use = "sampled texture type";
@@ -568,9 +604,7 @@
     if (subtype.errored)
       return Failure::kErrored;
 
-    return typ::Type{
-        builder_.create<ast::SampledTexture>(dim.value, subtype.value),
-        builder_.create<sem::SampledTexture>(dim.value, subtype.value)};
+    return builder_.ty.sampled_texture(source_range, dim.value, subtype.value);
   }
 
   auto ms_dim = multisampled_texture_type();
@@ -581,9 +615,8 @@
     if (subtype.errored)
       return Failure::kErrored;
 
-    return typ::Type{
-        builder_.create<ast::MultisampledTexture>(ms_dim.value, subtype.value),
-        builder_.create<sem::MultisampledTexture>(ms_dim.value, subtype.value)};
+    return builder_.ty.multisampled_texture(source_range, ms_dim.value,
+                                            subtype.value);
   }
 
   auto storage = storage_texture_type();
@@ -596,14 +629,8 @@
     if (format.errored)
       return Failure::kErrored;
 
-    auto* subtype = ast::StorageTexture::SubtypeFor(format.value, builder_);
-    auto* subtype_sem =
-        sem::StorageTexture::SubtypeFor(format.value, builder_.Types());
-
-    return typ::Type{builder_.create<ast::StorageTexture>(
-                         storage.value, format.value, subtype),
-                     builder_.create<sem::StorageTexture>(
-                         storage.value, format.value, subtype_sem)};
+    return builder_.ty.storage_texture(source_range, storage.value,
+                                       format.value);
   }
 
   return Failure::kNoMatch;
@@ -613,14 +640,12 @@
 //  : SAMPLER
 //  | SAMPLER_COMPARISON
 Maybe<typ::Type> ParserImpl::sampler_type() {
-  if (match(Token::Type::kSampler))
-    return typ::Type{builder_.create<ast::Sampler>(ast::SamplerKind::kSampler),
-                     builder_.create<sem::Sampler>(ast::SamplerKind::kSampler)};
+  Source source;
+  if (match(Token::Type::kSampler, &source))
+    return builder_.ty.sampler(source, ast::SamplerKind::kSampler);
 
-  if (match(Token::Type::kComparisonSampler))
-    return typ::Type{
-        builder_.create<ast::Sampler>(ast::SamplerKind::kComparisonSampler),
-        builder_.create<sem::Sampler>(ast::SamplerKind::kComparisonSampler)};
+  if (match(Token::Type::kComparisonSampler, &source))
+    return builder_.ty.sampler(source, ast::SamplerKind::kComparisonSampler);
 
   return Failure::kNoMatch;
 }
@@ -657,9 +682,9 @@
 // external_texture_type
 //  : TEXTURE_EXTERNAL
 Maybe<typ::Type> ParserImpl::external_texture_type() {
-  if (match(Token::Type::kTextureExternal)) {
-    return typ::Type{builder_.create<ast::ExternalTexture>(),
-                     builder_.create<sem::ExternalTexture>()};
+  Source source;
+  if (match(Token::Type::kTextureExternal, &source)) {
+    return builder_.ty.external_texture(source);
   }
 
   return Failure::kNoMatch;
@@ -698,25 +723,19 @@
 //  | TEXTURE_DEPTH_CUBE
 //  | TEXTURE_DEPTH_CUBE_ARRAY
 Maybe<typ::Type> ParserImpl::depth_texture_type() {
-  if (match(Token::Type::kTextureDepth2d))
-    return typ::Type{
-        builder_.create<ast::DepthTexture>(ast::TextureDimension::k2d),
-        builder_.create<sem::DepthTexture>(ast::TextureDimension::k2d)};
+  Source source;
 
-  if (match(Token::Type::kTextureDepth2dArray))
-    return typ::Type{
-        builder_.create<ast::DepthTexture>(ast::TextureDimension::k2dArray),
-        builder_.create<sem::DepthTexture>(ast::TextureDimension::k2dArray)};
+  if (match(Token::Type::kTextureDepth2d, &source))
+    return builder_.ty.depth_texture(source, ast::TextureDimension::k2d);
 
-  if (match(Token::Type::kTextureDepthCube))
-    return typ::Type{
-        builder_.create<ast::DepthTexture>(ast::TextureDimension::kCube),
-        builder_.create<sem::DepthTexture>(ast::TextureDimension::kCube)};
+  if (match(Token::Type::kTextureDepth2dArray, &source))
+    return builder_.ty.depth_texture(source, ast::TextureDimension::k2dArray);
 
-  if (match(Token::Type::kTextureDepthCubeArray))
-    return typ::Type{
-        builder_.create<ast::DepthTexture>(ast::TextureDimension::kCubeArray),
-        builder_.create<sem::DepthTexture>(ast::TextureDimension::kCubeArray)};
+  if (match(Token::Type::kTextureDepthCube, &source))
+    return builder_.ty.depth_texture(source, ast::TextureDimension::kCube);
+
+  if (match(Token::Type::kTextureDepthCubeArray, &source))
+    return builder_.ty.depth_texture(source, ast::TextureDimension::kCubeArray);
 
   return Failure::kNoMatch;
 }
@@ -897,26 +916,15 @@
   if (access_decos.size() > 1)
     return add_error(ident.source, "multiple access decorations not allowed");
 
-  // TODO(crbug.com/tint/724): Remove
-  auto* sem_ty = type.value.sem;
+  typ::Type ty = type.value;
+
   for (auto* deco : access_decos) {
     // If we have an access control decoration then we take it and wrap our
     // type up with that decoration
-    sem_ty = builder_.create<sem::AccessControl>(
-        deco->As<ast::AccessDecoration>()->value(), sem_ty);
+    ty = builder_.ty.access(deco->source(),
+                            deco->As<ast::AccessDecoration>()->value(), ty);
   }
-
-  auto* ty = type.value.ast;
-  // TODO(crbug.com/tint/724): Remove 'if'
-  if (ty) {
-    for (auto* deco : access_decos) {
-      // If we have an access control decoration then we take it and wrap our
-      // type up with that decoration
-      ty = builder_.create<ast::AccessControl>(
-          deco->As<ast::AccessDecoration>()->value(), ty);
-    }
-  }
-  return TypedIdentifier{typ::Type{ty, sem_ty}, ident.value, ident.source};
+  return TypedIdentifier{ty, ident.value, ident.source};
 }
 
 Expect<ast::AccessControl::Access> ParserImpl::expect_access_type() {
@@ -974,14 +982,10 @@
   if (!type.matched)
     return add_error(peek(), "invalid type alias");
 
-  // TODO(crbug.com/tint/724): remove
-  auto* alias = builder_.create<sem::Alias>(
-      builder_.Symbols().Register(name.value), type.value);
+  auto alias = builder_.ty.alias(make_source_range_from(t.source()), name.value,
+                                 type.value);
   register_constructed(name.value, alias);
-
-  return typ::Type{builder_.create<ast::Alias>(
-                       builder_.Symbols().Register(name.value), type.value),
-                   alias};
+  return alias;
 }
 
 // type_decl
@@ -1027,29 +1031,29 @@
 
 Maybe<typ::Type> ParserImpl::type_decl(ast::DecorationList& decos) {
   auto t = peek();
-  if (match(Token::Type::kIdentifier)) {
+  Source source;
+  if (match(Token::Type::kIdentifier, &source)) {
     // TODO(crbug.com/tint/697): Remove
     auto* ty = get_constructed(t.to_str());
     if (ty == nullptr)
       return add_error(t, "unknown constructed type '" + t.to_str() + "'");
 
-    return typ::Type{
-        builder_.create<ast::TypeName>(builder_.Symbols().Register(t.to_str())),
-        ty};
+    return typ::Type{builder_.create<ast::TypeName>(
+                         source, builder_.Symbols().Register(t.to_str())),
+                     ty};
   }
 
-  if (match(Token::Type::kBool))
-    return typ::Type{builder_.create<ast::Bool>(),
-                     builder_.create<sem::Bool>()};
+  if (match(Token::Type::kBool, &source))
+    return builder_.ty.bool_(source);
 
-  if (match(Token::Type::kF32))
-    return typ::Type{builder_.create<ast::F32>(), builder_.create<sem::F32>()};
+  if (match(Token::Type::kF32, &source))
+    return builder_.ty.f32(source);
 
-  if (match(Token::Type::kI32))
-    return typ::Type{builder_.create<ast::I32>(), builder_.create<sem::I32>()};
+  if (match(Token::Type::kI32, &source))
+    return builder_.ty.i32(source);
 
-  if (match(Token::Type::kU32))
-    return typ::Type{builder_.create<ast::U32>(), builder_.create<sem::U32>()};
+  if (match(Token::Type::kU32, &source))
+    return builder_.ty.u32(source);
 
   if (t.IsVec2() || t.IsVec3() || t.IsVec4()) {
     next();  // Consume the peek
@@ -1057,10 +1061,10 @@
   }
 
   if (match(Token::Type::kPtr))
-    return expect_type_decl_pointer();
+    return expect_type_decl_pointer(t);
 
-  if (match(Token::Type::kArray)) {
-    return expect_type_decl_array(std::move(decos));
+  if (match(Token::Type::kArray, &source)) {
+    return expect_type_decl_array(t, std::move(decos));
   }
 
   if (t.IsMat2x2() || t.IsMat2x3() || t.IsMat2x4() || t.IsMat3x2() ||
@@ -1088,24 +1092,33 @@
   return type.value;
 }
 
-Expect<typ::Type> ParserImpl::expect_type_decl_pointer() {
+Expect<typ::Type> ParserImpl::expect_type_decl_pointer(Token t) {
   const char* use = "ptr declaration";
 
-  return expect_lt_gt_block(use, [&]() -> Expect<typ::Type> {
+  ast::StorageClass storage_class = ast::StorageClass::kNone;
+
+  auto subtype = expect_lt_gt_block(use, [&]() -> Expect<typ::Type> {
     auto sc = expect_storage_class(use);
     if (sc.errored)
       return Failure::kErrored;
+    storage_class = sc.value;
 
     if (!expect(use, Token::Type::kComma))
       return Failure::kErrored;
 
-    auto subtype = expect_type(use);
-    if (subtype.errored)
+    auto type = expect_type(use);
+    if (type.errored)
       return Failure::kErrored;
 
-    return typ::Type{builder_.create<ast::Pointer>(subtype.value, sc.value),
-                     builder_.create<sem::Pointer>(subtype.value, sc.value)};
+    return type.value;
   });
+
+  if (subtype.errored) {
+    return Failure::kErrored;
+  }
+
+  return builder_.ty.pointer(make_source_range_from(t.source()), subtype.value,
+                             storage_class);
 }
 
 Expect<typ::Type> ParserImpl::expect_type_decl_vector(Token t) {
@@ -1121,20 +1134,22 @@
   if (subtype.errored)
     return Failure::kErrored;
 
-  return typ::Type{builder_.create<ast::Vector>(subtype.value.ast, count),
-                   builder_.create<sem::Vector>(subtype.value.sem, count)};
+  return builder_.ty.vec(make_source_range_from(t.source()), subtype.value,
+                         count);
 }
 
 Expect<typ::Type> ParserImpl::expect_type_decl_array(
+    Token t,
     ast::DecorationList decos) {
   const char* use = "array declaration";
 
-  return expect_lt_gt_block(use, [&]() -> Expect<typ::Type> {
-    auto subtype = expect_type(use);
-    if (subtype.errored)
+  uint32_t size = 0;
+
+  auto subtype = expect_lt_gt_block(use, [&]() -> Expect<typ::Type> {
+    auto type = expect_type(use);
+    if (type.errored)
       return Failure::kErrored;
 
-    uint32_t size = 0;
     if (match(Token::Type::kComma)) {
       auto val = expect_nonzero_positive_sint("array size");
       if (val.errored)
@@ -1142,10 +1157,15 @@
       size = val.value;
     }
 
-    return typ::Type{
-        create<ast::Array>(subtype.value, size, decos),
-        create<sem::ArrayType>(subtype.value, size, std::move(decos))};
+    return type.value;
   });
+
+  if (subtype.errored) {
+    return Failure::kErrored;
+  }
+
+  return builder_.ty.array(make_source_range_from(t.source()), subtype.value,
+                           size, std::move(decos));
 }
 
 Expect<typ::Type> ParserImpl::expect_type_decl_matrix(Token t) {
@@ -1168,8 +1188,8 @@
   if (subtype.errored)
     return Failure::kErrored;
 
-  return typ::Type{builder_.create<ast::Matrix>(subtype.value, rows, columns),
-                   builder_.create<sem::Matrix>(subtype.value, rows, columns)};
+  return builder_.ty.mat(make_source_range_from(t.source()), subtype.value,
+                         columns, rows);
 }
 
 // storage_class
@@ -1321,9 +1341,9 @@
 //   : type_decl
 //   | VOID
 Maybe<typ::Type> ParserImpl::function_type_decl() {
-  if (match(Token::Type::kVoid))
-    return typ::Type{builder_.create<ast::Void>(),
-                     builder_.create<sem::Void>()};
+  Source source;
+  if (match(Token::Type::kVoid, &source))
+    return builder_.ty.void_(source);
 
   return type_decl();
 }
@@ -3385,6 +3405,15 @@
   return result;
 }
 
+ParserImpl::MultiTokenSource ParserImpl::make_source_range() {
+  return MultiTokenSource(this);
+}
+
+ParserImpl::MultiTokenSource ParserImpl::make_source_range_from(
+    const Source& start) {
+  return MultiTokenSource(this, start);
+}
+
 }  // namespace wgsl
 }  // namespace reader
 }  // namespace tint
diff --git a/src/reader/wgsl/parser_impl.h b/src/reader/wgsl/parser_impl.h
index 7ae13ba..e01cf25 100644
--- a/src/reader/wgsl/parser_impl.h
+++ b/src/reader/wgsl/parser_impl.h
@@ -331,6 +331,8 @@
   /// @param idx the index of the token to return
   /// @returns the token `idx` positions ahead without advancing
   Token peek(size_t idx);
+  /// @returns the last token that was returned by `next()`
+  Token last_token() const;
   /// Appends an error at `t` with the message `msg`
   /// @param t the token to associate the error with
   /// @param msg the error message
@@ -821,9 +823,9 @@
   /// Used to ensure that all decorations are consumed.
   bool expect_decorations_consumed(const ast::DecorationList& list);
 
-  Expect<typ::Type> expect_type_decl_pointer();
+  Expect<typ::Type> expect_type_decl_pointer(Token t);
   Expect<typ::Type> expect_type_decl_vector(Token t);
-  Expect<typ::Type> expect_type_decl_array(ast::DecorationList decos);
+  Expect<typ::Type> expect_type_decl_array(Token t, ast::DecorationList decos);
   Expect<typ::Type> expect_type_decl_matrix(Token t);
 
   Expect<typ::Type> expect_type(const std::string& use);
@@ -832,6 +834,10 @@
   Maybe<ast::Statement*> for_header_initializer();
   Maybe<ast::Statement*> for_header_continuing();
 
+  class MultiTokenSource;
+  MultiTokenSource make_source_range();
+  MultiTokenSource make_source_range_from(const Source& start);
+
   /// Creates a new `ast::Node` owned by the Module. When the Module is
   /// destructed, the `ast::Node` will also be destructed.
   /// @param args the arguments to pass to the type constructor
@@ -843,6 +849,7 @@
 
   std::unique_ptr<Lexer> lexer_;
   std::deque<Token> token_queue_;
+  Token last_token_;
   bool synchronized_ = true;
   uint32_t sync_depth_ = 0;
   std::vector<Token::Type> sync_tokens_;
diff --git a/src/reader/wgsl/parser_impl_depth_texture_type_test.cc b/src/reader/wgsl/parser_impl_depth_texture_type_test.cc
index cc06e7a..82b73c1 100644
--- a/src/reader/wgsl/parser_impl_depth_texture_type_test.cc
+++ b/src/reader/wgsl/parser_impl_depth_texture_type_test.cc
@@ -38,6 +38,7 @@
   ASSERT_TRUE(t->Is<sem::DepthTexture>());
   EXPECT_EQ(t->As<sem::Texture>()->dim(), ast::TextureDimension::k2d);
   EXPECT_FALSE(p->has_error());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 17u}}));
 }
 
 TEST_F(ParserImplTest, DepthTextureType_2dArray) {
@@ -50,6 +51,7 @@
   ASSERT_TRUE(t->Is<sem::DepthTexture>());
   EXPECT_EQ(t->As<sem::Texture>()->dim(), ast::TextureDimension::k2dArray);
   EXPECT_FALSE(p->has_error());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 23u}}));
 }
 
 TEST_F(ParserImplTest, DepthTextureType_Cube) {
@@ -62,6 +64,7 @@
   ASSERT_TRUE(t->Is<sem::DepthTexture>());
   EXPECT_EQ(t->As<sem::Texture>()->dim(), ast::TextureDimension::kCube);
   EXPECT_FALSE(p->has_error());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 19u}}));
 }
 
 TEST_F(ParserImplTest, DepthTextureType_CubeArray) {
@@ -74,6 +77,7 @@
   ASSERT_TRUE(t->Is<sem::DepthTexture>());
   EXPECT_EQ(t->As<sem::Texture>()->dim(), ast::TextureDimension::kCubeArray);
   EXPECT_FALSE(p->has_error());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 25u}}));
 }
 
 }  // namespace
diff --git a/src/reader/wgsl/parser_impl_external_texture_type_test.cc b/src/reader/wgsl/parser_impl_external_texture_type_test.cc
index 92a8738..9f46112 100644
--- a/src/reader/wgsl/parser_impl_external_texture_type_test.cc
+++ b/src/reader/wgsl/parser_impl_external_texture_type_test.cc
@@ -32,6 +32,7 @@
   auto t = p->external_texture_type();
   EXPECT_TRUE(t.matched);
   EXPECT_FALSE(t.errored);
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 17u}}));
 }
 
 }  // namespace
diff --git a/src/reader/wgsl/parser_impl_function_type_decl_test.cc b/src/reader/wgsl/parser_impl_function_type_decl_test.cc
index f876f5c..ed4eb5a 100644
--- a/src/reader/wgsl/parser_impl_function_type_decl_test.cc
+++ b/src/reader/wgsl/parser_impl_function_type_decl_test.cc
@@ -29,6 +29,7 @@
   EXPECT_FALSE(e.errored);
   EXPECT_FALSE(p->has_error()) << p->error();
   ASSERT_EQ(e.value, v);
+  EXPECT_EQ(e.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 5u}}));
 }
 
 TEST_F(ParserImplTest, FunctionTypeDecl_Type) {
@@ -42,6 +43,7 @@
   EXPECT_FALSE(e.errored);
   EXPECT_FALSE(p->has_error()) << p->error();
   ASSERT_EQ(e.value, vec2);
+  EXPECT_EQ(e.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 10u}}));
 }
 
 TEST_F(ParserImplTest, FunctionTypeDecl_InvalidType) {
diff --git a/src/reader/wgsl/parser_impl_sampler_type_test.cc b/src/reader/wgsl/parser_impl_sampler_type_test.cc
index 4ec0c01..9ac9f6e 100644
--- a/src/reader/wgsl/parser_impl_sampler_type_test.cc
+++ b/src/reader/wgsl/parser_impl_sampler_type_test.cc
@@ -37,6 +37,7 @@
   ASSERT_TRUE(t->Is<sem::Sampler>());
   EXPECT_FALSE(t->As<sem::Sampler>()->IsComparison());
   EXPECT_FALSE(p->has_error());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 8u}}));
 }
 
 TEST_F(ParserImplTest, SamplerType_ComparisonSampler) {
@@ -48,6 +49,7 @@
   ASSERT_TRUE(t->Is<sem::Sampler>());
   EXPECT_TRUE(t->As<sem::Sampler>()->IsComparison());
   EXPECT_FALSE(p->has_error());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 19u}}));
 }
 
 }  // namespace
diff --git a/src/reader/wgsl/parser_impl_struct_member_test.cc b/src/reader/wgsl/parser_impl_struct_member_test.cc
index f92eb00..d1c8e4b 100644
--- a/src/reader/wgsl/parser_impl_struct_member_test.cc
+++ b/src/reader/wgsl/parser_impl_struct_member_test.cc
@@ -39,10 +39,8 @@
   EXPECT_EQ(m->type(), i32);
   EXPECT_EQ(m->decorations().size(), 0u);
 
-  ASSERT_EQ(m->source().range.begin.line, 1u);
-  ASSERT_EQ(m->source().range.begin.column, 1u);
-  ASSERT_EQ(m->source().range.end.line, 1u);
-  ASSERT_EQ(m->source().range.end.column, 2u);
+  EXPECT_EQ(m->source().range, (Source::Range{{1u, 1u}, {1u, 2u}}));
+  EXPECT_EQ(m->type().ast->source().range, (Source::Range{{1u, 5u}, {1u, 8u}}));
 }
 
 TEST_F(ParserImplTest, StructMember_ParsesWithOffsetDecoration_DEPRECATED) {
@@ -69,10 +67,9 @@
       m->decorations()[0]->As<ast::StructMemberOffsetDecoration>()->offset(),
       2u);
 
-  ASSERT_EQ(m->source().range.begin.line, 1u);
-  ASSERT_EQ(m->source().range.begin.column, 15u);
-  ASSERT_EQ(m->source().range.end.line, 1u);
-  ASSERT_EQ(m->source().range.end.column, 16u);
+  EXPECT_EQ(m->source().range, (Source::Range{{1u, 15u}, {1u, 16u}}));
+  EXPECT_EQ(m->type().ast->source().range,
+            (Source::Range{{1u, 19u}, {1u, 22u}}));
 }
 
 TEST_F(ParserImplTest, StructMember_ParsesWithAlignDecoration) {
@@ -98,10 +95,9 @@
   EXPECT_EQ(
       m->decorations()[0]->As<ast::StructMemberAlignDecoration>()->align(), 2u);
 
-  ASSERT_EQ(m->source().range.begin.line, 1u);
-  ASSERT_EQ(m->source().range.begin.column, 14u);
-  ASSERT_EQ(m->source().range.end.line, 1u);
-  ASSERT_EQ(m->source().range.end.column, 15u);
+  EXPECT_EQ(m->source().range, (Source::Range{{1u, 14u}, {1u, 15u}}));
+  EXPECT_EQ(m->type().ast->source().range,
+            (Source::Range{{1u, 18u}, {1u, 21u}}));
 }
 
 TEST_F(ParserImplTest, StructMember_ParsesWithSizeDecoration) {
@@ -127,10 +123,9 @@
   EXPECT_EQ(m->decorations()[0]->As<ast::StructMemberSizeDecoration>()->size(),
             2u);
 
-  ASSERT_EQ(m->source().range.begin.line, 1u);
-  ASSERT_EQ(m->source().range.begin.column, 13u);
-  ASSERT_EQ(m->source().range.end.line, 1u);
-  ASSERT_EQ(m->source().range.end.column, 14u);
+  EXPECT_EQ(m->source().range, (Source::Range{{1u, 13u}, {1u, 14u}}));
+  EXPECT_EQ(m->type().ast->source().range,
+            (Source::Range{{1u, 17u}, {1u, 20u}}));
 }
 
 TEST_F(ParserImplTest, StructMember_ParsesWithDecoration) {
@@ -156,10 +151,9 @@
   EXPECT_EQ(m->decorations()[0]->As<ast::StructMemberSizeDecoration>()->size(),
             2u);
 
-  ASSERT_EQ(m->source().range.begin.line, 1u);
-  ASSERT_EQ(m->source().range.begin.column, 13u);
-  ASSERT_EQ(m->source().range.end.line, 1u);
-  ASSERT_EQ(m->source().range.end.column, 14u);
+  EXPECT_EQ(m->source().range, (Source::Range{{1u, 13u}, {1u, 14u}}));
+  EXPECT_EQ(m->type().ast->source().range,
+            (Source::Range{{1u, 17u}, {1u, 20u}}));
 }
 
 TEST_F(ParserImplTest, StructMember_ParsesWithMultipleDecorations) {
@@ -189,10 +183,9 @@
   EXPECT_EQ(
       m->decorations()[1]->As<ast::StructMemberAlignDecoration>()->align(), 4u);
 
-  ASSERT_EQ(m->source().range.begin.line, 2u);
-  ASSERT_EQ(m->source().range.begin.column, 14u);
-  ASSERT_EQ(m->source().range.end.line, 2u);
-  ASSERT_EQ(m->source().range.end.column, 15u);
+  EXPECT_EQ(m->source().range, (Source::Range{{2u, 14u}, {2u, 15u}}));
+  EXPECT_EQ(m->type().ast->source().range,
+            (Source::Range{{2u, 18u}, {2u, 21u}}));
 }
 
 TEST_F(ParserImplTest, StructMember_InvalidDecoration) {
diff --git a/src/reader/wgsl/parser_impl_texture_sampler_types_test.cc b/src/reader/wgsl/parser_impl_texture_sampler_types_test.cc
index 8b5118e..646100d 100644
--- a/src/reader/wgsl/parser_impl_texture_sampler_types_test.cc
+++ b/src/reader/wgsl/parser_impl_texture_sampler_types_test.cc
@@ -40,6 +40,7 @@
   ASSERT_NE(t.value, nullptr);
   ASSERT_TRUE(t->Is<sem::Sampler>());
   ASSERT_FALSE(t->As<sem::Sampler>()->IsComparison());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 8u}}));
 }
 
 TEST_F(ParserImplTest, TextureSamplerTypes_SamplerComparison) {
@@ -51,6 +52,7 @@
   ASSERT_NE(t.value, nullptr);
   ASSERT_TRUE(t->Is<sem::Sampler>());
   ASSERT_TRUE(t->As<sem::Sampler>()->IsComparison());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 19u}}));
 }
 
 TEST_F(ParserImplTest, TextureSamplerTypes_DepthTexture) {
@@ -63,6 +65,7 @@
   ASSERT_TRUE(t->Is<sem::Texture>());
   ASSERT_TRUE(t->Is<sem::DepthTexture>());
   EXPECT_EQ(t->As<sem::Texture>()->dim(), ast::TextureDimension::k2d);
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 17u}}));
 }
 
 TEST_F(ParserImplTest, TextureSamplerTypes_SampledTexture_F32) {
@@ -76,6 +79,7 @@
   ASSERT_TRUE(t->Is<sem::SampledTexture>());
   ASSERT_TRUE(t->As<sem::SampledTexture>()->type()->Is<sem::F32>());
   EXPECT_EQ(t->As<sem::Texture>()->dim(), ast::TextureDimension::k1d);
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 16u}}));
 }
 
 TEST_F(ParserImplTest, TextureSamplerTypes_SampledTexture_I32) {
@@ -89,6 +93,7 @@
   ASSERT_TRUE(t->Is<sem::SampledTexture>());
   ASSERT_TRUE(t->As<sem::SampledTexture>()->type()->Is<sem::I32>());
   EXPECT_EQ(t->As<sem::Texture>()->dim(), ast::TextureDimension::k2d);
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 16u}}));
 }
 
 TEST_F(ParserImplTest, TextureSamplerTypes_SampledTexture_U32) {
@@ -102,6 +107,7 @@
   ASSERT_TRUE(t->Is<sem::SampledTexture>());
   ASSERT_TRUE(t->As<sem::SampledTexture>()->type()->Is<sem::U32>());
   EXPECT_EQ(t->As<sem::Texture>()->dim(), ast::TextureDimension::k3d);
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 16u}}));
 }
 
 TEST_F(ParserImplTest, TextureSamplerTypes_SampledTexture_Invalid) {
@@ -155,6 +161,7 @@
   ASSERT_TRUE(t->Is<sem::MultisampledTexture>());
   ASSERT_TRUE(t->As<sem::MultisampledTexture>()->type()->Is<sem::I32>());
   EXPECT_EQ(t->As<sem::Texture>()->dim(), ast::TextureDimension::k2d);
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 29u}}));
 }
 
 TEST_F(ParserImplTest, TextureSamplerTypes_MultisampledTexture_Invalid) {
@@ -210,6 +217,7 @@
   EXPECT_EQ(t->As<sem::StorageTexture>()->image_format(),
             ast::ImageFormat::kR8Unorm);
   EXPECT_EQ(t->As<sem::Texture>()->dim(), ast::TextureDimension::k1d);
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 28u}}));
 }
 
 TEST_F(ParserImplTest, TextureSamplerTypes_StorageTexture_Writeonly2dR16Float) {
@@ -225,6 +233,7 @@
   EXPECT_EQ(t->As<sem::StorageTexture>()->image_format(),
             ast::ImageFormat::kR16Float);
   EXPECT_EQ(t->As<sem::Texture>()->dim(), ast::TextureDimension::k2d);
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 29u}}));
 }
 
 TEST_F(ParserImplTest, TextureSamplerTypes_StorageTexture_InvalidType) {
diff --git a/src/reader/wgsl/parser_impl_type_alias_test.cc b/src/reader/wgsl/parser_impl_type_alias_test.cc
index 82cca4d..ad3e45e 100644
--- a/src/reader/wgsl/parser_impl_type_alias_test.cc
+++ b/src/reader/wgsl/parser_impl_type_alias_test.cc
@@ -33,6 +33,8 @@
   auto* alias = t->As<sem::Alias>();
   ASSERT_TRUE(alias->type()->Is<sem::I32>());
   ASSERT_EQ(alias->type(), i32);
+
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 13u}}));
 }
 
 TEST_F(ParserImplTest, TypeDecl_ParsesStruct_Ident) {
@@ -54,6 +56,8 @@
   auto* s = alias->type()->As<sem::StructType>();
   EXPECT_EQ(s->impl()->name(), p->builder().Symbols().Get("B"));
   EXPECT_EQ(s->impl()->name(), p->builder().Symbols().Get("B"));
+
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 11u}}));
 }
 
 TEST_F(ParserImplTest, TypeDecl_MissingIdent) {
diff --git a/src/reader/wgsl/parser_impl_type_decl_test.cc b/src/reader/wgsl/parser_impl_type_decl_test.cc
index 185ac69..8d7ebb1 100644
--- a/src/reader/wgsl/parser_impl_type_decl_test.cc
+++ b/src/reader/wgsl/parser_impl_type_decl_test.cc
@@ -50,6 +50,7 @@
   auto* alias = t->As<sem::Alias>();
   EXPECT_EQ(p->builder().Symbols().NameFor(alias->symbol()), "A");
   EXPECT_EQ(alias->type(), int_type);
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 2u}}));
 }
 
 TEST_F(ParserImplTest, TypeDecl_Identifier_NotFound) {
@@ -75,6 +76,7 @@
   ASSERT_NE(t.value, nullptr) << p->error();
   EXPECT_EQ(t.value, bool_type);
   ASSERT_TRUE(t->Is<sem::Bool>());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 5u}}));
 }
 
 TEST_F(ParserImplTest, TypeDecl_F32) {
@@ -89,6 +91,7 @@
   ASSERT_NE(t.value, nullptr) << p->error();
   EXPECT_EQ(t.value, float_type);
   ASSERT_TRUE(t->Is<sem::F32>());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 4u}}));
 }
 
 TEST_F(ParserImplTest, TypeDecl_I32) {
@@ -103,6 +106,7 @@
   ASSERT_NE(t.value, nullptr) << p->error();
   EXPECT_EQ(t.value, int_type);
   ASSERT_TRUE(t->Is<sem::I32>());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 4u}}));
 }
 
 TEST_F(ParserImplTest, TypeDecl_U32) {
@@ -117,11 +121,13 @@
   ASSERT_NE(t.value, nullptr) << p->error();
   EXPECT_EQ(t.value, uint_type);
   ASSERT_TRUE(t->Is<sem::U32>());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 4u}}));
 }
 
 struct VecData {
   const char* input;
   size_t count;
+  Source::Range range;
 };
 inline std::ostream& operator<<(std::ostream& out, VecData data) {
   out << std::string(data.input);
@@ -140,12 +146,16 @@
   ASSERT_FALSE(p->has_error());
   EXPECT_TRUE(t->Is<sem::Vector>());
   EXPECT_EQ(t->As<sem::Vector>()->size(), params.count);
+  EXPECT_EQ(t.value.ast->source().range, params.range);
 }
-INSTANTIATE_TEST_SUITE_P(ParserImplTest,
-                         VecTest,
-                         testing::Values(VecData{"vec2<f32>", 2},
-                                         VecData{"vec3<f32>", 3},
-                                         VecData{"vec4<f32>", 4}));
+INSTANTIATE_TEST_SUITE_P(
+    ParserImplTest,
+    VecTest,
+    testing::Values(VecData{"vec2<f32>", 2, Source::Range{{1u, 1u}, {1u, 10u}}},
+                    VecData{"vec3<f32>", 3, Source::Range{{1u, 1u}, {1u, 10u}}},
+                    VecData{"vec4<f32>", 4, Source::Range{{1u, 1u}, {1u, 10u}}}
+
+                    ));
 
 class VecMissingGreaterThanTest : public ParserImplTestWithParam<VecData> {};
 
@@ -161,9 +171,9 @@
 }
 INSTANTIATE_TEST_SUITE_P(ParserImplTest,
                          VecMissingGreaterThanTest,
-                         testing::Values(VecData{"vec2<f32", 2},
-                                         VecData{"vec3<f32", 3},
-                                         VecData{"vec4<f32", 4}));
+                         testing::Values(VecData{"vec2<f32", 2, {}},
+                                         VecData{"vec3<f32", 3, {}},
+                                         VecData{"vec4<f32", 4, {}}));
 
 class VecMissingLessThanTest : public ParserImplTestWithParam<VecData> {};
 
@@ -179,9 +189,9 @@
 }
 INSTANTIATE_TEST_SUITE_P(ParserImplTest,
                          VecMissingLessThanTest,
-                         testing::Values(VecData{"vec2", 2},
-                                         VecData{"vec3", 3},
-                                         VecData{"vec4", 4}));
+                         testing::Values(VecData{"vec2", 2, {}},
+                                         VecData{"vec3", 3, {}},
+                                         VecData{"vec4", 4, {}}));
 
 class VecBadType : public ParserImplTestWithParam<VecData> {};
 
@@ -197,9 +207,9 @@
 }
 INSTANTIATE_TEST_SUITE_P(ParserImplTest,
                          VecBadType,
-                         testing::Values(VecData{"vec2<unknown", 2},
-                                         VecData{"vec3<unknown", 3},
-                                         VecData{"vec4<unknown", 4}));
+                         testing::Values(VecData{"vec2<unknown", 2, {}},
+                                         VecData{"vec3<unknown", 3, {}},
+                                         VecData{"vec4<unknown", 4, {}}));
 
 class VecMissingType : public ParserImplTestWithParam<VecData> {};
 
@@ -215,9 +225,9 @@
 }
 INSTANTIATE_TEST_SUITE_P(ParserImplTest,
                          VecMissingType,
-                         testing::Values(VecData{"vec2<>", 2},
-                                         VecData{"vec3<>", 3},
-                                         VecData{"vec4<>", 4}));
+                         testing::Values(VecData{"vec2<>", 2, {}},
+                                         VecData{"vec3<>", 3, {}},
+                                         VecData{"vec4<>", 4, {}}));
 
 TEST_F(ParserImplTest, TypeDecl_Ptr) {
   auto p = parser("ptr<function, f32>");
@@ -231,6 +241,7 @@
   auto* ptr = t->As<sem::Pointer>();
   ASSERT_TRUE(ptr->type()->Is<sem::F32>());
   ASSERT_EQ(ptr->storage_class(), ast::StorageClass::kFunction);
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 19u}}));
 }
 
 TEST_F(ParserImplTest, TypeDecl_Ptr_ToVec) {
@@ -249,6 +260,7 @@
   auto* vec = ptr->type()->As<sem::Vector>();
   ASSERT_EQ(vec->size(), 2u);
   ASSERT_TRUE(vec->type()->Is<sem::F32>());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 25}}));
 }
 
 TEST_F(ParserImplTest, TypeDecl_Ptr_MissingLessThan) {
@@ -345,6 +357,7 @@
   ASSERT_EQ(a->size(), 5u);
   ASSERT_TRUE(a->type()->Is<sem::F32>());
   EXPECT_EQ(a->decorations().size(), 0u);
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 14u}}));
 }
 
 TEST_F(ParserImplTest, TypeDecl_Array_Stride) {
@@ -365,6 +378,7 @@
   auto* stride = a->decorations()[0];
   ASSERT_TRUE(stride->Is<ast::StrideDecoration>());
   ASSERT_EQ(stride->As<ast::StrideDecoration>()->stride(), 16u);
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 16u}, {1u, 29u}}));
 }
 
 TEST_F(ParserImplTest, TypeDecl_Array_Runtime_Stride) {
@@ -384,6 +398,7 @@
   auto* stride = a->decorations()[0];
   ASSERT_TRUE(stride->Is<ast::StrideDecoration>());
   ASSERT_EQ(stride->As<ast::StrideDecoration>()->stride(), 16u);
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 16u}, {1u, 26u}}));
 }
 
 TEST_F(ParserImplTest, TypeDecl_Array_MultipleDecorations_OneBlock) {
@@ -405,6 +420,7 @@
   EXPECT_EQ(decos[0]->As<ast::StrideDecoration>()->stride(), 16u);
   EXPECT_TRUE(decos[1]->Is<ast::StrideDecoration>());
   EXPECT_EQ(decos[1]->As<ast::StrideDecoration>()->stride(), 32u);
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 28u}, {1u, 38u}}));
 }
 
 TEST_F(ParserImplTest, TypeDecl_Array_MultipleDecorations_MultipleBlocks) {
@@ -426,6 +442,7 @@
   EXPECT_EQ(decos[0]->As<ast::StrideDecoration>()->stride(), 16u);
   EXPECT_TRUE(decos[1]->Is<ast::StrideDecoration>());
   EXPECT_EQ(decos[1]->As<ast::StrideDecoration>()->stride(), 32u);
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 31u}, {1u, 41u}}));
 }
 
 TEST_F(ParserImplTest, TypeDecl_Array_Decoration_MissingArray) {
@@ -522,6 +539,7 @@
   auto* a = t->As<sem::ArrayType>();
   ASSERT_TRUE(a->IsRuntimeArray());
   ASSERT_TRUE(a->type()->Is<sem::U32>());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 11u}}));
 }
 
 TEST_F(ParserImplTest, TypeDecl_Array_Runtime_Vec) {
@@ -536,6 +554,7 @@
   auto* a = t->As<sem::ArrayType>();
   ASSERT_TRUE(a->IsRuntimeArray());
   ASSERT_TRUE(a->type()->is_unsigned_integer_vector());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 17u}}));
 }
 
 TEST_F(ParserImplTest, TypeDecl_Array_BadType) {
@@ -612,6 +631,7 @@
   const char* input;
   size_t columns;
   size_t rows;
+  Source::Range range;
 };
 inline std::ostream& operator<<(std::ostream& out, MatrixData data) {
   out << std::string(data.input);
@@ -632,18 +652,21 @@
   auto* mat = t->As<sem::Matrix>();
   EXPECT_EQ(mat->rows(), params.rows);
   EXPECT_EQ(mat->columns(), params.columns);
+  EXPECT_EQ(t.value.ast->source().range, params.range);
 }
-INSTANTIATE_TEST_SUITE_P(ParserImplTest,
-                         MatrixTest,
-                         testing::Values(MatrixData{"mat2x2<f32>", 2, 2},
-                                         MatrixData{"mat2x3<f32>", 2, 3},
-                                         MatrixData{"mat2x4<f32>", 2, 4},
-                                         MatrixData{"mat3x2<f32>", 3, 2},
-                                         MatrixData{"mat3x3<f32>", 3, 3},
-                                         MatrixData{"mat3x4<f32>", 3, 4},
-                                         MatrixData{"mat4x2<f32>", 4, 2},
-                                         MatrixData{"mat4x3<f32>", 4, 3},
-                                         MatrixData{"mat4x4<f32>", 4, 4}));
+INSTANTIATE_TEST_SUITE_P(
+    ParserImplTest,
+    MatrixTest,
+    testing::Values(
+        MatrixData{"mat2x2<f32>", 2, 2, Source::Range{{1u, 1u}, {1u, 12u}}},
+        MatrixData{"mat2x3<f32>", 2, 3, Source::Range{{1u, 1u}, {1u, 12u}}},
+        MatrixData{"mat2x4<f32>", 2, 4, Source::Range{{1u, 1u}, {1u, 12u}}},
+        MatrixData{"mat3x2<f32>", 3, 2, Source::Range{{1u, 1u}, {1u, 12u}}},
+        MatrixData{"mat3x3<f32>", 3, 3, Source::Range{{1u, 1u}, {1u, 12u}}},
+        MatrixData{"mat3x4<f32>", 3, 4, Source::Range{{1u, 1u}, {1u, 12u}}},
+        MatrixData{"mat4x2<f32>", 4, 2, Source::Range{{1u, 1u}, {1u, 12u}}},
+        MatrixData{"mat4x3<f32>", 4, 3, Source::Range{{1u, 1u}, {1u, 12u}}},
+        MatrixData{"mat4x4<f32>", 4, 4, Source::Range{{1u, 1u}, {1u, 12u}}}));
 
 class MatrixMissingGreaterThanTest
     : public ParserImplTestWithParam<MatrixData> {};
@@ -660,15 +683,15 @@
 }
 INSTANTIATE_TEST_SUITE_P(ParserImplTest,
                          MatrixMissingGreaterThanTest,
-                         testing::Values(MatrixData{"mat2x2<f32", 2, 2},
-                                         MatrixData{"mat2x3<f32", 2, 3},
-                                         MatrixData{"mat2x4<f32", 2, 4},
-                                         MatrixData{"mat3x2<f32", 3, 2},
-                                         MatrixData{"mat3x3<f32", 3, 3},
-                                         MatrixData{"mat3x4<f32", 3, 4},
-                                         MatrixData{"mat4x2<f32", 4, 2},
-                                         MatrixData{"mat4x3<f32", 4, 3},
-                                         MatrixData{"mat4x4<f32", 4, 4}));
+                         testing::Values(MatrixData{"mat2x2<f32", 2, 2, {}},
+                                         MatrixData{"mat2x3<f32", 2, 3, {}},
+                                         MatrixData{"mat2x4<f32", 2, 4, {}},
+                                         MatrixData{"mat3x2<f32", 3, 2, {}},
+                                         MatrixData{"mat3x3<f32", 3, 3, {}},
+                                         MatrixData{"mat3x4<f32", 3, 4, {}},
+                                         MatrixData{"mat4x2<f32", 4, 2, {}},
+                                         MatrixData{"mat4x3<f32", 4, 3, {}},
+                                         MatrixData{"mat4x4<f32", 4, 4, {}}));
 
 class MatrixMissingLessThanTest : public ParserImplTestWithParam<MatrixData> {};
 
@@ -684,15 +707,15 @@
 }
 INSTANTIATE_TEST_SUITE_P(ParserImplTest,
                          MatrixMissingLessThanTest,
-                         testing::Values(MatrixData{"mat2x2 f32>", 2, 2},
-                                         MatrixData{"mat2x3 f32>", 2, 3},
-                                         MatrixData{"mat2x4 f32>", 2, 4},
-                                         MatrixData{"mat3x2 f32>", 3, 2},
-                                         MatrixData{"mat3x3 f32>", 3, 3},
-                                         MatrixData{"mat3x4 f32>", 3, 4},
-                                         MatrixData{"mat4x2 f32>", 4, 2},
-                                         MatrixData{"mat4x3 f32>", 4, 3},
-                                         MatrixData{"mat4x4 f32>", 4, 4}));
+                         testing::Values(MatrixData{"mat2x2 f32>", 2, 2, {}},
+                                         MatrixData{"mat2x3 f32>", 2, 3, {}},
+                                         MatrixData{"mat2x4 f32>", 2, 4, {}},
+                                         MatrixData{"mat3x2 f32>", 3, 2, {}},
+                                         MatrixData{"mat3x3 f32>", 3, 3, {}},
+                                         MatrixData{"mat3x4 f32>", 3, 4, {}},
+                                         MatrixData{"mat4x2 f32>", 4, 2, {}},
+                                         MatrixData{"mat4x3 f32>", 4, 3, {}},
+                                         MatrixData{"mat4x4 f32>", 4, 4, {}}));
 
 class MatrixBadType : public ParserImplTestWithParam<MatrixData> {};
 
@@ -706,17 +729,18 @@
   ASSERT_TRUE(p->has_error());
   ASSERT_EQ(p->error(), "1:8: unknown constructed type 'unknown'");
 }
-INSTANTIATE_TEST_SUITE_P(ParserImplTest,
-                         MatrixBadType,
-                         testing::Values(MatrixData{"mat2x2<unknown>", 2, 2},
-                                         MatrixData{"mat2x3<unknown>", 2, 3},
-                                         MatrixData{"mat2x4<unknown>", 2, 4},
-                                         MatrixData{"mat3x2<unknown>", 3, 2},
-                                         MatrixData{"mat3x3<unknown>", 3, 3},
-                                         MatrixData{"mat3x4<unknown>", 3, 4},
-                                         MatrixData{"mat4x2<unknown>", 4, 2},
-                                         MatrixData{"mat4x3<unknown>", 4, 3},
-                                         MatrixData{"mat4x4<unknown>", 4, 4}));
+INSTANTIATE_TEST_SUITE_P(
+    ParserImplTest,
+    MatrixBadType,
+    testing::Values(MatrixData{"mat2x2<unknown>", 2, 2, {}},
+                    MatrixData{"mat2x3<unknown>", 2, 3, {}},
+                    MatrixData{"mat2x4<unknown>", 2, 4, {}},
+                    MatrixData{"mat3x2<unknown>", 3, 2, {}},
+                    MatrixData{"mat3x3<unknown>", 3, 3, {}},
+                    MatrixData{"mat3x4<unknown>", 3, 4, {}},
+                    MatrixData{"mat4x2<unknown>", 4, 2, {}},
+                    MatrixData{"mat4x3<unknown>", 4, 3, {}},
+                    MatrixData{"mat4x4<unknown>", 4, 4, {}}));
 
 class MatrixMissingType : public ParserImplTestWithParam<MatrixData> {};
 
@@ -732,15 +756,15 @@
 }
 INSTANTIATE_TEST_SUITE_P(ParserImplTest,
                          MatrixMissingType,
-                         testing::Values(MatrixData{"mat2x2<>", 2, 2},
-                                         MatrixData{"mat2x3<>", 2, 3},
-                                         MatrixData{"mat2x4<>", 2, 4},
-                                         MatrixData{"mat3x2<>", 3, 2},
-                                         MatrixData{"mat3x3<>", 3, 3},
-                                         MatrixData{"mat3x4<>", 3, 4},
-                                         MatrixData{"mat4x2<>", 4, 2},
-                                         MatrixData{"mat4x3<>", 4, 3},
-                                         MatrixData{"mat4x4<>", 4, 4}));
+                         testing::Values(MatrixData{"mat2x2<>", 2, 2, {}},
+                                         MatrixData{"mat2x3<>", 2, 3, {}},
+                                         MatrixData{"mat2x4<>", 2, 4, {}},
+                                         MatrixData{"mat3x2<>", 3, 2, {}},
+                                         MatrixData{"mat3x3<>", 3, 3, {}},
+                                         MatrixData{"mat3x4<>", 3, 4, {}},
+                                         MatrixData{"mat4x2<>", 4, 2, {}},
+                                         MatrixData{"mat4x3<>", 4, 3, {}},
+                                         MatrixData{"mat4x4<>", 4, 4, {}}));
 
 TEST_F(ParserImplTest, TypeDecl_Sampler) {
   auto p = parser("sampler");
@@ -755,6 +779,7 @@
   EXPECT_EQ(t.value, type);
   ASSERT_TRUE(t->Is<sem::Sampler>());
   ASSERT_FALSE(t->As<sem::Sampler>()->IsComparison());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 8u}}));
 }
 
 TEST_F(ParserImplTest, TypeDecl_Texture) {
@@ -772,6 +797,7 @@
   ASSERT_TRUE(t->Is<sem::Texture>());
   ASSERT_TRUE(t->Is<sem::SampledTexture>());
   ASSERT_TRUE(t->As<sem::SampledTexture>()->type()->Is<sem::F32>());
+  EXPECT_EQ(t.value.ast->source().range, (Source::Range{{1u, 1u}, {1u, 18u}}));
 }
 
 }  // namespace
diff --git a/src/reader/wgsl/parser_impl_variable_decl_test.cc b/src/reader/wgsl/parser_impl_variable_decl_test.cc
index 19ed638..d932bd1 100644
--- a/src/reader/wgsl/parser_impl_variable_decl_test.cc
+++ b/src/reader/wgsl/parser_impl_variable_decl_test.cc
@@ -29,10 +29,8 @@
   EXPECT_NE(v->type, nullptr);
   EXPECT_TRUE(v->type->Is<sem::F32>());
 
-  EXPECT_EQ(v->source.range.begin.line, 1u);
-  EXPECT_EQ(v->source.range.begin.column, 5u);
-  EXPECT_EQ(v->source.range.end.line, 1u);
-  EXPECT_EQ(v->source.range.end.column, 11u);
+  EXPECT_EQ(v->source.range, (Source::Range{{1u, 5u}, {1u, 11u}}));
+  EXPECT_EQ(v->type.ast->source().range, (Source::Range{{1u, 14u}, {1u, 17u}}));
 }
 
 TEST_F(ParserImplTest, VariableDecl_MissingVar) {
diff --git a/src/reader/wgsl/parser_impl_variable_ident_decl_test.cc b/src/reader/wgsl/parser_impl_variable_ident_decl_test.cc
index 6802fb3..8d4ac62 100644
--- a/src/reader/wgsl/parser_impl_variable_ident_decl_test.cc
+++ b/src/reader/wgsl/parser_impl_variable_ident_decl_test.cc
@@ -30,10 +30,9 @@
   ASSERT_NE(decl->type, nullptr);
   ASSERT_TRUE(decl->type->Is<sem::F32>());
 
-  ASSERT_EQ(decl->source.range.begin.line, 1u);
-  ASSERT_EQ(decl->source.range.begin.column, 1u);
-  ASSERT_EQ(decl->source.range.end.line, 1u);
-  ASSERT_EQ(decl->source.range.end.column, 7u);
+  EXPECT_EQ(decl->source.range, (Source::Range{{1u, 1u}, {1u, 7u}}));
+  EXPECT_EQ(decl->type.ast->source().range,
+            (Source::Range{{1u, 10u}, {1u, 13u}}));
 }
 
 TEST_F(ParserImplTest, VariableIdentDecl_MissingIdent) {
diff --git a/src/resolver/resolver.cc b/src/resolver/resolver.cc
index ebb18c0..19acd68 100644
--- a/src/resolver/resolver.cc
+++ b/src/resolver/resolver.cc
@@ -84,13 +84,6 @@
   T old_value_;
 };
 
-// Helper function that returns the range union of two source locations. The
-// `start` and `end` locations are assumed to refer to the same source file.
-Source CombineSourceRange(const Source& start, const Source& end) {
-  return Source(Source::Range(start.range.begin, end.range.end),
-                start.file_path, start.file_content);
-}
-
 bool IsValidStorageTextureDimension(ast::TextureDimension dim) {
   switch (dim) {
     case ast::TextureDimension::k1d:
@@ -1392,7 +1385,7 @@
         "attempted to construct '" +
             vec_type->FriendlyName(builder_->Symbols()) + "' with " +
             std::to_string(value_cardinality_sum) + " component(s)",
-        CombineSourceRange(values_start, values_end));
+        Source::Combine(values_start, values_end));
     return false;
   }
   return true;
@@ -1414,7 +1407,7 @@
             VectorPretty(matrix_type->rows(), elem_type) + "' arguments in '" +
             matrix_type->FriendlyName(builder_->Symbols()) +
             "' constructor, found " + std::to_string(values.size()),
-        CombineSourceRange(values_start, values_end));
+        Source::Combine(values_start, values_end));
     return false;
   }
 
diff --git a/src/source.h b/src/source.h
index 07146da..b74d20e 100644
--- a/src/source.h
+++ b/src/source.h
@@ -18,6 +18,7 @@
 
 #include <iostream>
 #include <string>
+#include <tuple>
 #include <vector>
 
 namespace tint {
@@ -65,6 +66,27 @@
     size_t line = 0;
     /// the 1-based column number. 0 represents no column information.
     size_t column = 0;
+
+    /// Returns true of `this` location is lexicographically less than `rhs`
+    /// @param rhs location to compare against
+    /// @returns true if `this` < `rhs`
+    inline bool operator<(const Source::Location& rhs) {
+      return std::tie(line, column) < std::tie(rhs.line, rhs.column);
+    }
+
+    /// Returns true of `this` location is equal to `rhs`
+    /// @param rhs location to compare against
+    /// @returns true if `this` == `rhs`
+    inline bool operator==(const Location& rhs) const {
+      return line == rhs.line && column == rhs.column;
+    }
+
+    /// Returns true of `this` location is not equal to `rhs`
+    /// @param rhs location to compare against
+    /// @returns true if `this` != `rhs`
+    inline bool operator!=(const Location& rhs) const {
+      return !(*this == rhs);
+    }
   };
 
   /// Range holds a Location interval described by [begin, end).
@@ -75,12 +97,14 @@
 
     /// Constructs a zero-length Range starting at `loc`
     /// @param loc the start and end location for the range
-    inline explicit Range(const Location& loc) : begin(loc), end(loc) {}
+    inline constexpr explicit Range(const Location& loc)
+        : begin(loc), end(loc) {}
 
     /// Constructs the Range beginning at `b` and ending at `e`
     /// @param b the range start location
     /// @param e the range end location
-    inline Range(const Location& b, const Location& e) : begin(b), end(e) {}
+    inline constexpr Range(const Location& b, const Location& e)
+        : begin(b), end(e) {}
 
     /// Return a column-shifted Range
     /// @param n the number of characters to shift by
@@ -89,6 +113,18 @@
       return Range{{begin.line, begin.column + n}, {end.line, end.column + n}};
     }
 
+    /// Returns true of `this` range is not equal to `rhs`
+    /// @param rhs range to compare against
+    /// @returns true if `this` != `rhs`
+    inline bool operator==(const Range& rhs) const {
+      return begin == rhs.begin && end == rhs.end;
+    }
+
+    /// Returns true of `this` range is equal to `rhs`
+    /// @param rhs range to compare against
+    /// @returns true if `this` == `rhs`
+    inline bool operator!=(const Range& rhs) const { return !(*this == rhs); }
+
     /// The location of the first character in the range.
     Location begin;
     /// The location of one-past the last character in the range.
@@ -139,6 +175,29 @@
     return Source(range + n, file_path, file_content);
   }
 
+  /// Returns true of `this` Source is lexicographically less than `rhs`
+  /// @param rhs source to compare against
+  /// @returns true if `this` < `rhs`
+  inline bool operator<(const Source& rhs) {
+    if (file_path != rhs.file_path) {
+      return false;
+    }
+    if (file_content != rhs.file_content) {
+      return false;
+    }
+    return range.begin < rhs.range.begin;
+  }
+
+  /// Helper function that returns the range union of two source locations. The
+  /// `start` and `end` locations are assumed to refer to the same source file.
+  /// @param start the start source of the range
+  /// @param end the end source of the range
+  /// @returns the combined source
+  inline static Source Combine(const Source& start, const Source& end) {
+    return Source(Source::Range(start.range.begin, end.range.end),
+                  start.file_path, start.file_content);
+  }
+
   /// range is the span of text this source refers to in #file_path
   Range range;
   /// file is the optional file path this source refers to
@@ -147,6 +206,25 @@
   const FileContent* file_content = nullptr;
 };
 
+/// Writes the Source::Location to the std::ostream.
+/// @param out the std::ostream to write to
+/// @param loc the location to write
+/// @returns out so calls can be chained
+inline std::ostream& operator<<(std::ostream& out,
+                                const Source::Location& loc) {
+  out << loc.line << ":" << loc.column;
+  return out;
+}
+
+/// Writes the Source::Range to the std::ostream.
+/// @param out the std::ostream to write to
+/// @param range the range to write
+/// @returns out so calls can be chained
+inline std::ostream& operator<<(std::ostream& out, const Source::Range& range) {
+  out << "[" << range.begin << ", " << range.end << "]";
+  return out;
+}
+
 /// Writes the Source to the std::ostream.
 /// @param out the std::ostream to write to
 /// @param source the source to write
diff --git a/src/typepair.h b/src/typepair.h
index cc1c724..7e53800 100644
--- a/src/typepair.h
+++ b/src/typepair.h
@@ -33,6 +33,7 @@
 class Array;
 class Bool;
 class DepthTexture;
+class ExternalTexture;
 class F32;
 class I32;
 class Matrix;
@@ -54,6 +55,7 @@
 class ArrayType;
 class Bool;
 class DepthTexture;
+class ExternalTexture;
 class F32;
 class I32;
 class Matrix;
@@ -145,6 +147,7 @@
 using Array = TypePair<ast::Array, sem::ArrayType>;
 using Bool = TypePair<ast::Bool, sem::Bool>;
 using DepthTexture = TypePair<ast::DepthTexture, sem::DepthTexture>;
+using ExternalTexture = TypePair<ast::ExternalTexture, sem::ExternalTexture>;
 using F32 = TypePair<ast::F32, sem::F32>;
 using I32 = TypePair<ast::I32, sem::I32>;
 using Matrix = TypePair<ast::Matrix, sem::Matrix>;