tint: Add ast::TemplatedIdentifier

Will be used to replace all type identifiers that take templated arguments.

Bug: tint:1810
Change-Id: I31ad8dc4826375a783143cc33f336d8a4860613c
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/117893
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
Commit-Queue: Ben Clayton <bclayton@chromium.org>
Reviewed-by: James Price <jrprice@google.com>
Kokoro: Ben Clayton <bclayton@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index f7e4854..481c267 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -315,6 +315,7 @@
     "ast/struct_member_offset_attribute.h",
     "ast/struct_member_size_attribute.h",
     "ast/switch_statement.h",
+    "ast/templated_identifier.h",
     "ast/texture.h",
     "ast/traverse_expressions.h",
     "ast/type.h",
@@ -697,6 +698,8 @@
     "ast/struct_member_size_attribute.h",
     "ast/switch_statement.cc",
     "ast/switch_statement.h",
+    "ast/templated_identifier.cc",
+    "ast/templated_identifier.h",
     "ast/texture.cc",
     "ast/texture.h",
     "ast/traverse_expressions.h",
@@ -1358,6 +1361,7 @@
       "ast/struct_member_test.cc",
       "ast/struct_test.cc",
       "ast/switch_statement_test.cc",
+      "ast/templated_identifier_test.cc",
       "ast/test_helper.h",
       "ast/texture_test.cc",
       "ast/traverse_expressions_test.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 2223d7d..bdb1bbf 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -227,6 +227,8 @@
   ast/struct.h
   ast/switch_statement.cc
   ast/switch_statement.h
+  ast/templated_identifier.cc
+  ast/templated_identifier.h
   ast/texture.cc
   ast/texture.h
   ast/traverse_expressions.h
@@ -880,6 +882,7 @@
     ast/struct_test.cc
     ast/switch_statement_test.cc
     ast/test_helper.h
+    ast/templated_identifier_test.cc
     ast/texture_test.cc
     ast/traverse_expressions_test.cc
     ast/type_name_test.cc
diff --git a/src/tint/ast/diagnostic_control.cc b/src/tint/ast/diagnostic_control.cc
index a814d58..6d6ed09 100644
--- a/src/tint/ast/diagnostic_control.cc
+++ b/src/tint/ast/diagnostic_control.cc
@@ -30,6 +30,19 @@
 
 namespace tint::ast {
 
+DiagnosticControl::DiagnosticControl(ProgramID pid,
+                                     NodeID nid,
+                                     const Source& src,
+                                     DiagnosticSeverity sev,
+                                     const Identifier* rule)
+    : Base(pid, nid, src), severity(sev), rule_name(rule) {
+    TINT_ASSERT(AST, rule != nullptr);
+    if (rule) {
+        // It is invalid for a diagnostic rule name to be templated
+        TINT_ASSERT(AST, !rule->Is<TemplatedIdentifier>());
+    }
+}
+
 DiagnosticControl::~DiagnosticControl() = default;
 
 const DiagnosticControl* DiagnosticControl::Clone(CloneContext* ctx) const {
diff --git a/src/tint/ast/diagnostic_control.cc.tmpl b/src/tint/ast/diagnostic_control.cc.tmpl
index c6dc463..c7ca2b8 100644
--- a/src/tint/ast/diagnostic_control.cc.tmpl
+++ b/src/tint/ast/diagnostic_control.cc.tmpl
@@ -20,6 +20,19 @@
 
 namespace tint::ast {
 
+DiagnosticControl::DiagnosticControl(ProgramID pid,
+                                     NodeID nid,
+                                     const Source& src,
+                                     DiagnosticSeverity sev,
+                                     const Identifier* rule)
+        : Base(pid, nid, src), severity(sev), rule_name(rule) {
+    TINT_ASSERT(AST, rule != nullptr);
+    if (rule) {
+        // It is invalid for a diagnostic rule name to be templated
+        TINT_ASSERT(AST, !rule->Is<TemplatedIdentifier>());
+    }
+}
+
 DiagnosticControl::~DiagnosticControl() = default;
 
 const DiagnosticControl* DiagnosticControl::Clone(CloneContext* ctx) const {
diff --git a/src/tint/ast/diagnostic_control.h b/src/tint/ast/diagnostic_control.h
index d1df329..6cf27b9 100644
--- a/src/tint/ast/diagnostic_control.h
+++ b/src/tint/ast/diagnostic_control.h
@@ -103,8 +103,7 @@
                       NodeID nid,
                       const Source& src,
                       DiagnosticSeverity sev,
-                      const Identifier* rule)
-        : Base(pid, nid, src), severity(sev), rule_name(rule) {}
+                      const Identifier* rule);
 
     ~DiagnosticControl() override;
 
diff --git a/src/tint/ast/diagnostic_control.h.tmpl b/src/tint/ast/diagnostic_control.h.tmpl
index c106a3e..9b8f1fe 100644
--- a/src/tint/ast/diagnostic_control.h.tmpl
+++ b/src/tint/ast/diagnostic_control.h.tmpl
@@ -51,8 +51,7 @@
                       NodeID nid,
                       const Source& src,
                       DiagnosticSeverity sev,
-                      const Identifier* rule)
-        : Base(pid, nid, src), severity(sev), rule_name(rule) {}
+                      const Identifier* rule);
 
     ~DiagnosticControl() override;
 
diff --git a/src/tint/ast/diagnostic_control_test.cc b/src/tint/ast/diagnostic_control_test.cc
index 798d187..742bee3 100644
--- a/src/tint/ast/diagnostic_control_test.cc
+++ b/src/tint/ast/diagnostic_control_test.cc
@@ -22,6 +22,7 @@
 
 #include <string>
 
+#include "gtest/gtest-spi.h"
 #include "src/tint/ast/diagnostic_control.h"
 #include "src/tint/ast/test_helper.h"
 
@@ -44,6 +45,16 @@
     EXPECT_EQ(control->rule_name, name);
 }
 
+TEST_F(DiagnosticControlTest, Assert_RuleNotTemplated) {
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder b;
+            b.create<ast::DiagnosticControl>(DiagnosticSeverity::kWarning,
+                                             b.Ident("name", "a", "b", "c"));
+        },
+        "internal compiler error");
+}
+
 namespace diagnostic_severity_tests {
 
 namespace parse_print_tests {
diff --git a/src/tint/ast/diagnostic_control_test.cc.tmpl b/src/tint/ast/diagnostic_control_test.cc.tmpl
index 74ff73e..2eebba3 100644
--- a/src/tint/ast/diagnostic_control_test.cc.tmpl
+++ b/src/tint/ast/diagnostic_control_test.cc.tmpl
@@ -12,6 +12,7 @@
 
 #include <string>
 
+#include "gtest/gtest-spi.h"
 #include "src/tint/ast/diagnostic_control.h"
 #include "src/tint/ast/test_helper.h"
 
@@ -25,8 +26,7 @@
     Source source;
     source.range.begin = Source::Location{20, 2};
     source.range.end = Source::Location{20, 5};
-    auto* control = create<ast::DiagnosticControl>(source,
-                                                   DiagnosticSeverity::kWarning, name);
+    auto* control = create<ast::DiagnosticControl>(source, DiagnosticSeverity::kWarning, name);
     EXPECT_EQ(control->source.range.begin.line, 20u);
     EXPECT_EQ(control->source.range.begin.column, 2u);
     EXPECT_EQ(control->source.range.end.line, 20u);
@@ -35,6 +35,16 @@
     EXPECT_EQ(control->rule_name, name);
 }
 
+TEST_F(DiagnosticControlTest, Assert_RuleNotTemplated) {
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder b;
+            b.create<ast::DiagnosticControl>(DiagnosticSeverity::kWarning,
+                                             b.Ident("name", "a", "b", "c"));
+        },
+        "internal compiler error");
+}
+
 namespace diagnostic_severity_tests {
 
 {{ Eval "TestParsePrintEnum" (Sem.Enum "diagnostic_severity")}}
diff --git a/src/tint/ast/identifier.h b/src/tint/ast/identifier.h
index 9176403..0d99b00 100644
--- a/src/tint/ast/identifier.h
+++ b/src/tint/ast/identifier.h
@@ -20,7 +20,7 @@
 namespace tint::ast {
 
 /// An identifier
-class Identifier final : public Castable<Identifier, ast::Node> {
+class Identifier : public Castable<Identifier, ast::Node> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/identifier_expression.cc b/src/tint/ast/identifier_expression.cc
index ca4e0cb..1f86d9f 100644
--- a/src/tint/ast/identifier_expression.cc
+++ b/src/tint/ast/identifier_expression.cc
@@ -27,6 +27,10 @@
     : Base(pid, nid, src), identifier(ident) {
     TINT_ASSERT(AST, identifier != nullptr);
     TINT_ASSERT_PROGRAM_IDS_EQUAL(AST, identifier, program_id);
+
+    // It is currently invalid for a templated identifier expression to be used as an identifier
+    // expression, as this should parse as a ast::TypeName.
+    TINT_ASSERT(AST, !ident->Is<TemplatedIdentifier>());
 }
 
 IdentifierExpression::IdentifierExpression(IdentifierExpression&&) = default;
diff --git a/src/tint/ast/identifier_expression_test.cc b/src/tint/ast/identifier_expression_test.cc
index 3bef4ca..80742a2 100644
--- a/src/tint/ast/identifier_expression_test.cc
+++ b/src/tint/ast/identifier_expression_test.cc
@@ -57,5 +57,14 @@
         "internal compiler error");
 }
 
+TEST_F(IdentifierExpressionTest, Assert_IdentifierNotTemplated) {
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder b;
+            b.create<IdentifierExpression>(b.Ident("ident", "a", "b", "c"));
+        },
+        "internal compiler error");
+}
+
 }  // namespace
 }  // namespace tint::ast
diff --git a/src/tint/ast/member_accessor_expression.cc b/src/tint/ast/member_accessor_expression.cc
index f414f0d..81ef085 100644
--- a/src/tint/ast/member_accessor_expression.cc
+++ b/src/tint/ast/member_accessor_expression.cc
@@ -30,6 +30,11 @@
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, structure, program_id);
     TINT_ASSERT(AST, member);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, member, program_id);
+
+    // It is currently invalid for a structure to hold a templated member
+    if (member) {
+        TINT_ASSERT(AST, !member->Is<TemplatedIdentifier>());
+    }
 }
 
 MemberAccessorExpression::MemberAccessorExpression(MemberAccessorExpression&&) = default;
diff --git a/src/tint/ast/member_accessor_expression_test.cc b/src/tint/ast/member_accessor_expression_test.cc
index bfc7218..1b05eeb 100644
--- a/src/tint/ast/member_accessor_expression_test.cc
+++ b/src/tint/ast/member_accessor_expression_test.cc
@@ -80,5 +80,15 @@
         "internal compiler error");
 }
 
+TEST_F(MemberAccessorExpressionTest, Assert_MemberNotTemplated) {
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder b;
+            b.create<MemberAccessorExpression>(b.Expr("structure"),
+                                               b.Ident("member", "a", "b", "c"));
+        },
+        "internal compiler error");
+}
+
 }  // namespace
 }  // namespace tint::ast
diff --git a/src/tint/ast/templated_identifier.cc b/src/tint/ast/templated_identifier.cc
new file mode 100644
index 0000000..756b183
--- /dev/null
+++ b/src/tint/ast/templated_identifier.cc
@@ -0,0 +1,48 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/templated_identifier.h"
+
+#include <utility>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::TemplatedIdentifier);
+
+namespace tint::ast {
+
+TemplatedIdentifier::TemplatedIdentifier(ProgramID pid,
+                                         NodeID nid,
+                                         const Source& src,
+                                         const Symbol& sym,
+                                         utils::VectorRef<const ast::Expression*> args)
+    : Base(pid, nid, src, sym), arguments(std::move(args)) {
+    for (auto* arg : arguments) {
+        TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, arg, program_id);
+    }
+}
+
+TemplatedIdentifier::TemplatedIdentifier(TemplatedIdentifier&&) = default;
+
+TemplatedIdentifier::~TemplatedIdentifier() = default;
+
+const TemplatedIdentifier* TemplatedIdentifier::Clone(CloneContext* ctx) const {
+    // Clone arguments outside of create() call to have deterministic ordering
+    auto src = ctx->Clone(source);
+    auto sym = ctx->Clone(symbol);
+    auto args = ctx->Clone(arguments);
+    return ctx->dst->create<TemplatedIdentifier>(src, sym, args);
+}
+
+}  // namespace tint::ast
diff --git a/src/tint/ast/templated_identifier.h b/src/tint/ast/templated_identifier.h
new file mode 100644
index 0000000..7a49c05
--- /dev/null
+++ b/src/tint/ast/templated_identifier.h
@@ -0,0 +1,56 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_TEMPLATED_IDENTIFIER_H_
+#define SRC_TINT_AST_TEMPLATED_IDENTIFIER_H_
+
+#include "src/tint/ast/identifier.h"
+
+// Forward declarations
+namespace tint::ast {
+class Expression;
+}  // namespace tint::ast
+
+namespace tint::ast {
+
+/// A templated identifier expression
+class TemplatedIdentifier final : public Castable<TemplatedIdentifier, Identifier> {
+  public:
+    /// Constructor
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
+    /// @param src the source of this node
+    /// @param sym the symbol for the identifier
+    /// @param args the template arguments
+    TemplatedIdentifier(ProgramID pid,
+                        NodeID nid,
+                        const Source& src,
+                        const Symbol& sym,
+                        utils::VectorRef<const Expression*> args);
+    /// Move constructor
+    TemplatedIdentifier(TemplatedIdentifier&&);
+    ~TemplatedIdentifier() override;
+
+    /// Clones this node and all transitive child nodes using the `CloneContext` `ctx`.
+    /// @param ctx the clone context
+    /// @return the newly cloned node
+    const TemplatedIdentifier* Clone(CloneContext* ctx) const override;
+
+    /// The templated arguments
+    const utils::Vector<const Expression*, 3> arguments;
+};
+
+}  // namespace tint::ast
+
+#endif  // SRC_TINT_AST_TEMPLATED_IDENTIFIER_H_
diff --git a/src/tint/ast/templated_identifier_test.cc b/src/tint/ast/templated_identifier_test.cc
new file mode 100644
index 0000000..648c334
--- /dev/null
+++ b/src/tint/ast/templated_identifier_test.cc
@@ -0,0 +1,80 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest-spi.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint::ast {
+namespace {
+
+using namespace tint::number_suffixes;  // NOLINT
+
+using TemplatedIdentifierTest = TestHelper;
+
+TEST_F(TemplatedIdentifierTest, Creation) {
+    auto* i = Ident("ident", 1_a, Add("x", "y"), false, "x");
+    EXPECT_EQ(i->symbol, Symbols().Get("ident"));
+    ASSERT_EQ(i->arguments.Length(), 4u);
+    EXPECT_TRUE(i->arguments[0]->Is<ast::IntLiteralExpression>());
+    EXPECT_TRUE(i->arguments[1]->Is<ast::BinaryExpression>());
+    EXPECT_TRUE(i->arguments[2]->Is<ast::BoolLiteralExpression>());
+    EXPECT_TRUE(i->arguments[3]->Is<ast::IdentifierExpression>());
+}
+
+TEST_F(TemplatedIdentifierTest, Creation_WithSource) {
+    auto* i = Ident(Source{{20, 2}}, "ident", 1_a, Add("x", "y"), false, "x");
+    EXPECT_EQ(i->symbol, Symbols().Get("ident"));
+    ASSERT_EQ(i->arguments.Length(), 4u);
+    EXPECT_TRUE(i->arguments[0]->Is<ast::IntLiteralExpression>());
+    EXPECT_TRUE(i->arguments[1]->Is<ast::BinaryExpression>());
+    EXPECT_TRUE(i->arguments[2]->Is<ast::BoolLiteralExpression>());
+    EXPECT_TRUE(i->arguments[3]->Is<ast::IdentifierExpression>());
+
+    auto src = i->source;
+    EXPECT_EQ(src.range.begin.line, 20u);
+    EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(TemplatedIdentifierTest, Assert_InvalidSymbol) {
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder b;
+            b.Expr("");
+        },
+        "internal compiler error");
+}
+
+TEST_F(TemplatedIdentifierTest, Assert_DifferentProgramID_Symbol) {
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder b1;
+            ProgramBuilder b2;
+            b1.Ident(b2.Sym("b2"), b1.Expr(1_i));
+        },
+        "internal compiler error");
+}
+
+TEST_F(TemplatedIdentifierTest, Assert_DifferentProgramID_TemplateArg) {
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder b1;
+            ProgramBuilder b2;
+            b1.Ident("b1", b2.Expr(1_i));
+        },
+        "internal compiler error");
+}
+
+}  // namespace
+}  // namespace tint::ast
diff --git a/src/tint/ast/type_name_test.cc b/src/tint/ast/type_name_test.cc
index 803dad3..9e01f0c 100644
--- a/src/tint/ast/type_name_test.cc
+++ b/src/tint/ast/type_name_test.cc
@@ -28,6 +28,17 @@
     EXPECT_EQ(t->name->symbol, Symbols().Get("ty"));
 }
 
+TEST_F(TypeNameTest, Creation_Templated) {
+    auto* t = ty.type_name("ty", 1_a, 2._a, false);
+    auto* name = As<ast::TemplatedIdentifier>(t->name);
+    ASSERT_NE(name, nullptr);
+    EXPECT_EQ(name->symbol, Symbols().Get("ty"));
+    ASSERT_EQ(name->arguments.Length(), 3u);
+    EXPECT_TRUE(name->arguments[0]->Is<ast::IntLiteralExpression>());
+    EXPECT_TRUE(name->arguments[1]->Is<ast::FloatLiteralExpression>());
+    EXPECT_TRUE(name->arguments[2]->Is<ast::BoolLiteralExpression>());
+}
+
 TEST_F(TypeNameTest, Creation_WithSource) {
     auto* t = ty.type_name(Source{{20, 2}}, "ty");
     ASSERT_NE(t->name, nullptr);
diff --git a/src/tint/program_builder.h b/src/tint/program_builder.h
index 7954a41..dc3560a 100644
--- a/src/tint/program_builder.h
+++ b/src/tint/program_builder.h
@@ -81,6 +81,7 @@
 #include "src/tint/ast/struct_member_offset_attribute.h"
 #include "src/tint/ast/struct_member_size_attribute.h"
 #include "src/tint/ast/switch_statement.h"
+#include "src/tint/ast/templated_identifier.h"
 #include "src/tint/ast/type_name.h"
 #include "src/tint/ast/u32.h"
 #include "src/tint/ast/unary_op_expression.h"
@@ -204,9 +205,7 @@
         template <typename... ARGS>
         explicit LetOptions(ARGS&&... args) {
             static constexpr bool has_init =
-                (traits::IsTypeOrDerived<std::remove_pointer_t<std::remove_reference_t<ARGS>>,
-                                         ast::Expression> ||
-                 ...);
+                (traits::IsTypeOrDerived<traits::PtrElTy<ARGS>, ast::Expression> || ...);
             static_assert(has_init, "Let() must be constructed with an initializer expression");
             (Set(std::forward<ARGS>(args)), ...);
         }
@@ -229,9 +228,7 @@
         template <typename... ARGS>
         explicit ConstOptions(ARGS&&... args) {
             static constexpr bool has_init =
-                (traits::IsTypeOrDerived<std::remove_pointer_t<std::remove_reference_t<ARGS>>,
-                                         ast::Expression> ||
-                 ...);
+                (traits::IsTypeOrDerived<traits::PtrElTy<ARGS>, ast::Expression> || ...);
             static_assert(has_init, "Const() must be constructed with an initializer expression");
             (Set(std::forward<ARGS>(args)), ...);
         }
@@ -904,19 +901,23 @@
 
         /// Creates a type name
         /// @param name the name
+        /// @param args the optional template arguments
         /// @returns the type name
-        template <typename NAME>
-        const ast::TypeName* type_name(NAME&& name) const {
-            return builder->create<ast::TypeName>(builder->Ident(std::forward<NAME>(name)));
+        template <typename NAME, typename... ARGS, typename _ = DisableIfSource<NAME>>
+        const ast::TypeName* type_name(NAME&& name, ARGS&&... args) const {
+            return builder->create<ast::TypeName>(
+                builder->Ident(std::forward<NAME>(name), std::forward<ARGS>(args)...));
         }
 
         /// Creates a type name
         /// @param source the Source of the node
         /// @param name the name
+        /// @param args the optional template arguments
         /// @returns the type name
-        template <typename NAME>
-        const ast::TypeName* type_name(const Source& source, NAME&& name) const {
-            return builder->create<ast::TypeName>(source, builder->Ident(std::forward<NAME>(name)));
+        template <typename NAME, typename... ARGS>
+        const ast::TypeName* type_name(const Source& source, NAME&& name, ARGS&&... args) const {
+            return builder->create<ast::TypeName>(
+                source, builder->Ident(std::forward<NAME>(name), std::forward<ARGS>(args)...));
         }
 
         /// Creates an alias type
@@ -1150,22 +1151,30 @@
 
     /// @param source the source information
     /// @param identifier the identifier symbol
+    /// @param args optional templated identifier arguments
     /// @return an ast::Identifier with the given symbol
-    template <typename IDENTIFIER>
-    const ast::Identifier* Ident(const Source& source, IDENTIFIER&& identifier) {
-        return create<ast::Identifier>(source, Sym(std::forward<IDENTIFIER>(identifier)));
+    template <typename IDENTIFIER, typename... ARGS>
+    const auto* Ident(const Source& source, IDENTIFIER&& identifier, ARGS&&... args) {
+        Symbol sym = Sym(std::forward<IDENTIFIER>(identifier));
+        if constexpr (sizeof...(args) > 0) {
+            return create<ast::TemplatedIdentifier>(source, sym,
+                                                    ExprList(std::forward<ARGS>(args)...));
+        } else {
+            return create<ast::Identifier>(source, sym);
+        }
     }
 
     /// @param identifier the identifier symbol
+    /// @param args optional templated identifier arguments
     /// @return an ast::Identifier with the given symbol
-    template <typename IDENTIFIER>
-    const ast::Identifier* Ident(IDENTIFIER&& identifier) {
-        if constexpr (traits::IsTypeOrDerived<
-                          std::decay_t<std::remove_pointer_t<std::decay_t<IDENTIFIER>>>,
-                          ast::Identifier>) {
+    template <typename IDENTIFIER, typename... ARGS, typename = DisableIfSource<IDENTIFIER>>
+    const auto* Ident(IDENTIFIER&& identifier, ARGS&&... args) {
+        if constexpr (traits::IsTypeOrDerived<traits::PtrElTy<IDENTIFIER>, ast::Identifier>) {
+            static_assert(sizeof...(args) == 0);
             return identifier;  // Pass-through
         } else {
-            return create<ast::Identifier>(Sym(std::forward<IDENTIFIER>(identifier)));
+            return Ident(source_, std::forward<IDENTIFIER>(identifier),
+                         std::forward<ARGS>(args)...);
         }
     }
 
@@ -1232,6 +1241,16 @@
         return create<ast::IdentifierExpression>(Ident(variable->symbol));
     }
 
+    /// @param ident the identifier
+    /// @return an ast::IdentifierExpression with the given identifier
+    template <typename IDENTIFIER, typename = traits::EnableIfIsType<IDENTIFIER, ast::Identifier>>
+    const ast::IdentifierExpression* Expr(const IDENTIFIER* ident) {
+        static_assert(!traits::IsType<IDENTIFIER, ast::TemplatedIdentifier>,
+                      "it is currently invalid for a templated identifier expression to be used as "
+                      "an identifier expression, as this should parse as an ast::TypeName");
+        return create<ast::IdentifierExpression>(ident);
+    }
+
     /// @param source the source information
     /// @param value the boolean value
     /// @return a Scalar constructor for the given value
@@ -2347,43 +2366,46 @@
     }
 
     /// @param source the source information
-    /// @param obj the object for the index accessor expression
-    /// @param idx the index argument for the index accessor expression
-    /// @returns a `ast::IndexAccessorExpression` that indexes `arr` with `idx`
-    template <typename OBJ, typename IDX>
-    const ast::IndexAccessorExpression* IndexAccessor(const Source& source, OBJ&& obj, IDX&& idx) {
-        return create<ast::IndexAccessorExpression>(source, Expr(std::forward<OBJ>(obj)),
-                                                    Expr(std::forward<IDX>(idx)));
+    /// @param object the object for the index accessor expression
+    /// @param index the index argument for the index accessor expression
+    /// @returns a `ast::IndexAccessorExpression` that indexes @p object with @p index
+    template <typename OBJECT, typename INDEX>
+    const ast::IndexAccessorExpression* IndexAccessor(const Source& source,
+                                                      OBJECT&& object,
+                                                      INDEX&& index) {
+        return create<ast::IndexAccessorExpression>(source, Expr(std::forward<OBJECT>(object)),
+                                                    Expr(std::forward<INDEX>(index)));
     }
 
-    /// @param obj the object for the index accessor expression
-    /// @param idx the index argument for the index accessor expression
-    /// @returns a `ast::IndexAccessorExpression` that indexes `arr` with `idx`
-    template <typename OBJ, typename IDX>
-    const ast::IndexAccessorExpression* IndexAccessor(OBJ&& obj, IDX&& idx) {
-        return create<ast::IndexAccessorExpression>(Expr(std::forward<OBJ>(obj)),
-                                                    Expr(std::forward<IDX>(idx)));
+    /// @param object the object for the index accessor expression
+    /// @param index the index argument for the index accessor expression
+    /// @returns a `ast::IndexAccessorExpression` that indexes @p object with @p index
+    template <typename OBJECT, typename INDEX>
+    const ast::IndexAccessorExpression* IndexAccessor(OBJECT&& object, INDEX&& index) {
+        return create<ast::IndexAccessorExpression>(Expr(std::forward<OBJECT>(object)),
+                                                    Expr(std::forward<INDEX>(index)));
     }
 
     /// @param source the source information
-    /// @param obj the object for the member accessor expression
-    /// @param idx the index argument for the member accessor expression
-    /// @returns a `ast::MemberAccessorExpression` that indexes `obj` with `idx`
-    template <typename OBJ, typename IDX>
+    /// @param object the object for the member accessor expression
+    /// @param member the member argument for the member accessor expression
+    /// @returns a `ast::MemberAccessorExpression` that indexes @p object with @p member
+    template <typename OBJECT, typename MEMBER>
     const ast::MemberAccessorExpression* MemberAccessor(const Source& source,
-                                                        OBJ&& obj,
-                                                        IDX&& idx) {
-        return create<ast::MemberAccessorExpression>(source, Expr(std::forward<OBJ>(obj)),
-                                                     Ident(std::forward<IDX>(idx)));
+                                                        OBJECT&& object,
+                                                        MEMBER&& member) {
+        static_assert(!traits::IsType<traits::PtrElTy<MEMBER>, ast::TemplatedIdentifier>,
+                      "it is currently invalid for a structure to hold a templated member");
+        return create<ast::MemberAccessorExpression>(source, Expr(std::forward<OBJECT>(object)),
+                                                     Ident(std::forward<MEMBER>(member)));
     }
 
-    /// @param obj the object for the member accessor expression
-    /// @param idx the index argument for the member accessor expression
-    /// @returns a `ast::MemberAccessorExpression` that indexes `obj` with `idx`
-    template <typename OBJ, typename IDX>
-    const ast::MemberAccessorExpression* MemberAccessor(OBJ&& obj, IDX&& idx) {
-        return create<ast::MemberAccessorExpression>(Expr(std::forward<OBJ>(obj)),
-                                                     Ident(std::forward<IDX>(idx)));
+    /// @param object the object for the member accessor expression
+    /// @param member the member argument for the member accessor expression
+    /// @returns a `ast::MemberAccessorExpression` that indexes @p object with @p member
+    template <typename OBJECT, typename MEMBER>
+    const ast::MemberAccessorExpression* MemberAccessor(OBJECT&& object, MEMBER&& member) {
+        return MemberAccessor(source_, std::forward<OBJECT>(object), std::forward<MEMBER>(member));
     }
 
     /// Creates a ast::StructMemberOffsetAttribute
@@ -3284,6 +3306,8 @@
     const ast::DiagnosticAttribute* DiagnosticAttribute(const Source& source,
                                                         ast::DiagnosticSeverity severity,
                                                         NAME&& rule_name) {
+        static_assert(!traits::IsType<traits::PtrElTy<NAME>, ast::TemplatedIdentifier>,
+                      "it is invalid for a diagnostic rule name to be templated");
         return create<ast::DiagnosticAttribute>(
             source, DiagnosticControl(source, severity, std::forward<NAME>(rule_name)));
     }
diff --git a/src/tint/resolver/dependency_graph.cc b/src/tint/resolver/dependency_graph.cc
index 6dc111e..d09a7e3 100644
--- a/src/tint/resolver/dependency_graph.cc
+++ b/src/tint/resolver/dependency_graph.cc
@@ -61,6 +61,7 @@
 #include "src/tint/ast/struct_member_offset_attribute.h"
 #include "src/tint/ast/struct_member_size_attribute.h"
 #include "src/tint/ast/switch_statement.h"
+#include "src/tint/ast/templated_identifier.h"
 #include "src/tint/ast/traverse_expressions.h"
 #include "src/tint/ast/type_name.h"
 #include "src/tint/ast/u32.h"
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 36f9da8..1630642 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -321,6 +321,10 @@
         [&](const ast::TypeName* t) -> type::Type* {
             Mark(t->name);
 
+            if (t->name->Is<ast::TemplatedIdentifier>()) {
+                TINT_UNREACHABLE(Resolver, builder_->Diagnostics()) << "TODO(crbug.com/tint/1810)";
+            }
+
             auto* resolved = sem_.ResolvedSymbol(t);
             if (resolved == nullptr) {
                 if (IsBuiltin(t->name->symbol)) {
diff --git a/src/tint/traits.h b/src/tint/traits.h
index f6dac27..1d0c2f6 100644
--- a/src/tint/traits.h
+++ b/src/tint/traits.h
@@ -15,6 +15,7 @@
 #ifndef SRC_TINT_TRAITS_H_
 #define SRC_TINT_TRAITS_H_
 
+#include <string>
 #include <tuple>
 #include <type_traits>
 #include <utility>
@@ -177,6 +178,16 @@
 template <typename T, typename TypeContainer>
 static constexpr bool IsTypeIn = detail::IsTypeIn<T, TypeContainer>::value;
 
+/// Evaluates to the decayed pointer element type, or the decayed type T if T is not a pointer.
+template <typename T>
+using PtrElTy = Decay<std::remove_pointer_t<Decay<T>>>;
+
+/// Evaluates to true if `T` decayed is a `std::string`, `std::string_view` or `const char*`
+template <typename T>
+static constexpr bool IsStringLike =
+    std::is_same_v<Decay<T>, std::string> || std::is_same_v<Decay<T>, std::string_view> ||
+    std::is_same_v<Decay<T>, const char*>;
+
 }  // namespace tint::traits
 
 #endif  // SRC_TINT_TRAITS_H_
diff --git a/src/tint/traits_test.cc b/src/tint/traits_test.cc
index c100107..e86e389 100644
--- a/src/tint/traits_test.cc
+++ b/src/tint/traits_test.cc
@@ -19,6 +19,25 @@
 namespace tint::traits {
 
 namespace {
+
+static_assert(std::is_same_v<PtrElTy<int*>, int>);
+static_assert(std::is_same_v<PtrElTy<int const*>, int>);
+static_assert(std::is_same_v<PtrElTy<int const* const>, int>);
+static_assert(std::is_same_v<PtrElTy<int const* const volatile>, int>);
+static_assert(std::is_same_v<PtrElTy<int>, int>);
+static_assert(std::is_same_v<PtrElTy<int const>, int>);
+static_assert(std::is_same_v<PtrElTy<int const volatile>, int>);
+
+static_assert(IsStringLike<std::string>);
+static_assert(IsStringLike<std::string_view>);
+static_assert(IsStringLike<const char*>);
+static_assert(IsStringLike<const std::string&>);
+static_assert(IsStringLike<const std::string_view&>);
+static_assert(IsStringLike<const char*>);
+static_assert(!IsStringLike<bool>);
+static_assert(!IsStringLike<int>);
+static_assert(!IsStringLike<const char**>);
+
 struct S {};
 void F1(S) {}
 void F3(int, S, float) {}