[Tint] Implement context dependent builtin values

Built-in value names were made context dependent in
https://github.com/gpuweb/gpuweb/pull/4559. The changes to the grammar
and AST resolution are now reflected in Tint, so the corresponding
previously failing CTS tests pass.

Bug: 42251275
Change-Id: I586ff25603fe4fc68d9d0ba22ab720c84d9309d8
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/197477
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Commit-Queue: Natalie Chouinard <chouinard@google.com>
diff --git a/src/tint/lang/msl/writer/ast_raise/subgroup_ballot.cc b/src/tint/lang/msl/writer/ast_raise/subgroup_ballot.cc
index 781e245..77f401c 100644
--- a/src/tint/lang/msl/writer/ast_raise/subgroup_ballot.cc
+++ b/src/tint/lang/msl/writer/ast_raise/subgroup_ballot.cc
@@ -157,10 +157,7 @@
         Symbol subgroup_size;
         for (auto* param : ep->params) {
             auto* builtin = ast::GetAttribute<ast::BuiltinAttribute>(param->attributes);
-            if (builtin &&
-                src.Sem()
-                        .Get<sem::BuiltinEnumExpression<core::BuiltinValue>>(builtin->builtin)
-                        ->Value() == core::BuiltinValue::kSubgroupSize) {
+            if (builtin && src.Sem().Get(builtin)->Value() == core::BuiltinValue::kSubgroupSize) {
                 subgroup_size = ctx.Clone(param->name->symbol);
             }
         }
diff --git a/src/tint/lang/spirv/writer/ast_printer/builder.cc b/src/tint/lang/spirv/writer/ast_printer/builder.cc
index 9a42478..1d925a2 100644
--- a/src/tint/lang/spirv/writer/ast_printer/builder.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/builder.cc
@@ -50,6 +50,7 @@
 #include "src/tint/lang/wgsl/ast/traverse_expressions.h"
 #include "src/tint/lang/wgsl/helpers/append_vector.h"
 #include "src/tint/lang/wgsl/helpers/check_supported_extensions.h"
+#include "src/tint/lang/wgsl/sem/builtin_enum_expression.h"
 #include "src/tint/lang/wgsl/sem/builtin_fn.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/function.h"
diff --git a/src/tint/lang/wgsl/ast/BUILD.bazel b/src/tint/lang/wgsl/ast/BUILD.bazel
index e038bf6..9baea14 100644
--- a/src/tint/lang/wgsl/ast/BUILD.bazel
+++ b/src/tint/lang/wgsl/ast/BUILD.bazel
@@ -52,6 +52,7 @@
     "break_statement.cc",
     "builder.cc",
     "builtin_attribute.cc",
+    "builtin_value_name.cc",
     "call_expression.cc",
     "call_statement.cc",
     "case_selector.cc",
@@ -133,6 +134,7 @@
     "break_statement.h",
     "builder.h",
     "builtin_attribute.h",
+    "builtin_value_name.h",
     "call_expression.h",
     "call_statement.h",
     "case_selector.h",
@@ -241,6 +243,7 @@
     "builtin_attribute_test.cc",
     "builtin_texture_helper_test.cc",
     "builtin_texture_helper_test.h",
+    "builtin_value_name_test.cc",
     "call_expression_test.cc",
     "call_statement_test.cc",
     "case_selector_test.cc",
diff --git a/src/tint/lang/wgsl/ast/BUILD.cmake b/src/tint/lang/wgsl/ast/BUILD.cmake
index 6afc090..de65c9c 100644
--- a/src/tint/lang/wgsl/ast/BUILD.cmake
+++ b/src/tint/lang/wgsl/ast/BUILD.cmake
@@ -67,6 +67,8 @@
   lang/wgsl/ast/builder.h
   lang/wgsl/ast/builtin_attribute.cc
   lang/wgsl/ast/builtin_attribute.h
+  lang/wgsl/ast/builtin_value_name.cc
+  lang/wgsl/ast/builtin_value_name.h
   lang/wgsl/ast/call_expression.cc
   lang/wgsl/ast/call_expression.h
   lang/wgsl/ast/call_statement.cc
@@ -241,6 +243,7 @@
   lang/wgsl/ast/builtin_attribute_test.cc
   lang/wgsl/ast/builtin_texture_helper_test.cc
   lang/wgsl/ast/builtin_texture_helper_test.h
+  lang/wgsl/ast/builtin_value_name_test.cc
   lang/wgsl/ast/call_expression_test.cc
   lang/wgsl/ast/call_statement_test.cc
   lang/wgsl/ast/case_selector_test.cc
diff --git a/src/tint/lang/wgsl/ast/BUILD.gn b/src/tint/lang/wgsl/ast/BUILD.gn
index a1b43d7..05c02bb 100644
--- a/src/tint/lang/wgsl/ast/BUILD.gn
+++ b/src/tint/lang/wgsl/ast/BUILD.gn
@@ -70,6 +70,8 @@
     "builder.h",
     "builtin_attribute.cc",
     "builtin_attribute.h",
+    "builtin_value_name.cc",
+    "builtin_value_name.h",
     "call_expression.cc",
     "call_expression.h",
     "call_statement.cc",
@@ -241,6 +243,7 @@
       "builtin_attribute_test.cc",
       "builtin_texture_helper_test.cc",
       "builtin_texture_helper_test.h",
+      "builtin_value_name_test.cc",
       "call_expression_test.cc",
       "call_statement_test.cc",
       "case_selector_test.cc",
diff --git a/src/tint/lang/wgsl/ast/builder.h b/src/tint/lang/wgsl/ast/builder.h
index 33fbe7a..9fd60a8 100644
--- a/src/tint/lang/wgsl/ast/builder.h
+++ b/src/tint/lang/wgsl/ast/builder.h
@@ -47,6 +47,7 @@
 #include "src/tint/lang/wgsl/ast/bool_literal_expression.h"
 #include "src/tint/lang/wgsl/ast/break_if_statement.h"
 #include "src/tint/lang/wgsl/ast/break_statement.h"
+#include "src/tint/lang/wgsl/ast/builtin_value_name.h"
 #include "src/tint/lang/wgsl/ast/call_expression.h"
 #include "src/tint/lang/wgsl/ast/call_statement.h"
 #include "src/tint/lang/wgsl/ast/case_statement.h"
@@ -3054,13 +3055,44 @@
     /// @returns the selector pointer
     const ast::CaseSelector* DefaultCaseSelector() { return create<ast::CaseSelector>(nullptr); }
 
+    /// Passthrough overload
+    /// @param name the builtin value name
+    /// @returns @p name
+    const ast::BuiltinValueName* BuiltinValueName(const ast::BuiltinValueName* name) {
+        return name;
+    }
+
+    /// Creates an ast::BuiltinValueName
+    /// @param name the builtin value name
+    /// @returns the builtin value name
+    template <typename NAME>
+    const ast::BuiltinValueName* BuiltinValueName(NAME&& name) {
+        static_assert(!traits::IsType<traits::PtrElTy<NAME>, ast::TemplatedIdentifier>,
+                      "it is invalid for a builtin value name to be templated");
+        auto* name_ident = Ident(std::forward<NAME>(name));
+        return create<ast::BuiltinValueName>(name_ident ? name_ident->source : source_, name_ident);
+    }
+
+    /// Creates an ast::BuiltinValueName
+    /// @param source the source information
+    /// @param name the builtin value name
+    /// @returns the builtin value name
+    template <typename NAME>
+    const ast::BuiltinValueName* BuiltinValueName(const Source& source, NAME&& name) {
+        static_assert(!traits::IsType<traits::PtrElTy<NAME>, ast::TemplatedIdentifier>,
+                      "it is invalid for a builtin value name to be templated");
+        auto* name_ident = Ident(std::forward<NAME>(name));
+        return create<ast::BuiltinValueName>(source, name_ident);
+    }
+
     /// Creates an ast::BuiltinAttribute
     /// @param source the source information
     /// @param builtin the builtin value
     /// @returns the builtin attribute pointer
     template <typename BUILTIN>
     const ast::BuiltinAttribute* Builtin(const Source& source, BUILTIN&& builtin) {
-        return create<ast::BuiltinAttribute>(source, Expr(std::forward<BUILTIN>(builtin)));
+        return create<ast::BuiltinAttribute>(source,
+                                             BuiltinValueName(std::forward<BUILTIN>(builtin)));
     }
 
     /// Creates an ast::BuiltinAttribute
@@ -3068,7 +3100,8 @@
     /// @returns the builtin attribute pointer
     template <typename BUILTIN>
     const ast::BuiltinAttribute* Builtin(BUILTIN&& builtin) {
-        return create<ast::BuiltinAttribute>(source_, Expr(std::forward<BUILTIN>(builtin)));
+        return create<ast::BuiltinAttribute>(source_,
+                                             BuiltinValueName(std::forward<BUILTIN>(builtin)));
     }
 
     /// Creates an ast::InterpolateAttribute
diff --git a/src/tint/lang/wgsl/ast/builtin_attribute.cc b/src/tint/lang/wgsl/ast/builtin_attribute.cc
index 7f5c0ca..b39ecd2 100644
--- a/src/tint/lang/wgsl/ast/builtin_attribute.cc
+++ b/src/tint/lang/wgsl/ast/builtin_attribute.cc
@@ -30,6 +30,7 @@
 #include <string>
 
 #include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/builtin_value_name.h"
 #include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::BuiltinAttribute);
@@ -39,7 +40,7 @@
 BuiltinAttribute::BuiltinAttribute(GenerationID pid,
                                    NodeID nid,
                                    const Source& src,
-                                   const Expression* b)
+                                   const BuiltinValueName* b)
     : Base(pid, nid, src), builtin(b) {
     TINT_ASSERT_GENERATION_IDS_EQUAL(b, generation_id);
 }
diff --git a/src/tint/lang/wgsl/ast/builtin_attribute.h b/src/tint/lang/wgsl/ast/builtin_attribute.h
index 08815d7..8f80137 100644
--- a/src/tint/lang/wgsl/ast/builtin_attribute.h
+++ b/src/tint/lang/wgsl/ast/builtin_attribute.h
@@ -34,7 +34,7 @@
 
 // Forward declarations
 namespace tint::ast {
-class Expression;
+class BuiltinValueName;
 }
 
 namespace tint::ast {
@@ -47,7 +47,10 @@
     /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param builtin the builtin value
-    BuiltinAttribute(GenerationID pid, NodeID nid, const Source& src, const Expression* builtin);
+    BuiltinAttribute(GenerationID pid,
+                     NodeID nid,
+                     const Source& src,
+                     const BuiltinValueName* builtin);
     ~BuiltinAttribute() override;
 
     /// @returns the WGSL name for the attribute
@@ -60,7 +63,7 @@
     const BuiltinAttribute* Clone(CloneContext& ctx) const override;
 
     /// The builtin value
-    const Expression* const builtin;
+    const BuiltinValueName* const builtin;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/builtin_attribute_test.cc b/src/tint/lang/wgsl/ast/builtin_attribute_test.cc
index 42aefe7..5561224 100644
--- a/src/tint/lang/wgsl/ast/builtin_attribute_test.cc
+++ b/src/tint/lang/wgsl/ast/builtin_attribute_test.cc
@@ -26,6 +26,7 @@
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 #include "src/tint/lang/core/builtin_value.h"
+#include "src/tint/lang/wgsl/ast/builtin_value_name.h"
 #include "src/tint/lang/wgsl/ast/helper_test.h"
 
 namespace tint::ast {
@@ -36,7 +37,7 @@
 
 TEST_F(BuiltinAttributeTest, Creation) {
     auto* d = Builtin(core::BuiltinValue::kFragDepth);
-    CheckIdentifier(d->builtin, "frag_depth");
+    CheckIdentifier(d->builtin->name, "frag_depth");
 }
 
 TEST_F(BuiltinAttributeDeathTest, Assert_Null_Builtin) {
@@ -53,7 +54,7 @@
         {
             ProgramBuilder b1;
             ProgramBuilder b2;
-            b1.Builtin(b2.Expr("bang"));
+            b1.Builtin(b2.BuiltinValueName("bang"));
         },
         "internal compiler error");
 }
diff --git a/src/tint/lang/wgsl/ast/builtin_value_name.cc b/src/tint/lang/wgsl/ast/builtin_value_name.cc
new file mode 100644
index 0000000..28a94a1
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/builtin_value_name.cc
@@ -0,0 +1,58 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ast/builtin_value_name.h"
+
+#include <string>
+
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::BuiltinValueName);
+
+namespace tint::ast {
+
+BuiltinValueName::BuiltinValueName(GenerationID pid,
+                                   NodeID nid,
+                                   const Source& src,
+                                   const Identifier* n)
+    : Base(pid, nid, src), name(n) {
+    TINT_ASSERT(name != nullptr);
+    TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(name, generation_id);
+}
+
+const BuiltinValueName* BuiltinValueName::Clone(CloneContext& ctx) const {
+    auto src = ctx.Clone(source);
+    auto n = ctx.Clone(name);
+    return ctx.dst->create<BuiltinValueName>(src, n);
+}
+
+std::string BuiltinValueName::String() const {
+    return name->symbol.Name();
+}
+
+}  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/builtin_value_name.h b/src/tint/lang/wgsl/ast/builtin_value_name.h
new file mode 100644
index 0000000..5655967
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/builtin_value_name.h
@@ -0,0 +1,66 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_WGSL_AST_BUILTIN_VALUE_NAME_H_
+#define SRC_TINT_LANG_WGSL_AST_BUILTIN_VALUE_NAME_H_
+
+#include <string>
+
+#include "src/tint/lang/wgsl/ast/node.h"
+
+// Forward declarations
+namespace tint::ast {
+class Identifier;
+}  // namespace tint::ast
+
+namespace tint::ast {
+
+/// A builtin value name used for builtin value attributes.
+class BuiltinValueName final : public Castable<BuiltinValueName, Node> {
+  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 name the rule name
+    BuiltinValueName(GenerationID pid, NodeID nid, const Source& src, const Identifier* name);
+
+    /// Clones this node and all transitive child nodes using the `CloneContext` `ctx`.
+    /// @param ctx the clone context
+    /// @return the newly cloned node
+    const BuiltinValueName* Clone(CloneContext& ctx) const override;
+
+    /// @return the full name of this builtin value.
+    std::string String() const;
+
+    /// The builtin value name.
+    Identifier const* const name;
+};
+
+}  // namespace tint::ast
+
+#endif  // SRC_TINT_LANG_WGSL_AST_BUILTIN_VALUE_NAME_H_
diff --git a/src/tint/lang/wgsl/ast/builtin_value_name_test.cc b/src/tint/lang/wgsl/ast/builtin_value_name_test.cc
new file mode 100644
index 0000000..22d859b
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/builtin_value_name_test.cc
@@ -0,0 +1,63 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ast/builtin_value_name.h"
+#include "src/tint/lang/core/builtin_value.h"
+#include "src/tint/lang/wgsl/ast/helper_test.h"
+
+namespace tint::ast {
+namespace {
+
+using BuiltinValueNameTest = TestHelper;
+using BuiltinValueNameDeathTest = BuiltinValueNameTest;
+
+TEST_F(BuiltinValueNameTest, Creation) {
+    auto* builtin = BuiltinValueName(core::BuiltinValue::kNumWorkgroups);
+    CheckIdentifier(builtin->name, "num_workgroups");
+}
+
+TEST_F(BuiltinValueNameDeathTest, Assert_Null) {
+    EXPECT_DEATH_IF_SUPPORTED(
+        {
+            ProgramBuilder b;
+            b.BuiltinValueName(nullptr);
+        },
+        "internal compiler error");
+}
+
+TEST_F(BuiltinValueNameDeathTest, Assert_DifferentGenerationID) {
+    EXPECT_DEATH_IF_SUPPORTED(
+        {
+            ProgramBuilder b1;
+            ProgramBuilder b2;
+            b1.BuiltinValueName(b2.Ident("bang"));
+        },
+        "internal compiler error");
+}
+
+}  // namespace
+}  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/transform/renamer.cc b/src/tint/lang/wgsl/ast/transform/renamer.cc
index 25485b2..296f66c 100644
--- a/src/tint/lang/wgsl/ast/transform/renamer.cc
+++ b/src/tint/lang/wgsl/ast/transform/renamer.cc
@@ -1340,6 +1340,9 @@
                     [&](const sem::ValueConstructor*) {
                         preserve_if_builtin_type(call->target->identifier);
                     });
+            },
+            [&](const BuiltinAttribute* builtin) {
+                preserved_identifiers.Add(builtin->builtin->name);
             });
     }
 
diff --git a/src/tint/lang/wgsl/ls/sem_tokens.cc b/src/tint/lang/wgsl/ls/sem_tokens.cc
index c389b8c..7340c1f 100644
--- a/src/tint/lang/wgsl/ls/sem_tokens.cc
+++ b/src/tint/lang/wgsl/ls/sem_tokens.cc
@@ -39,6 +39,7 @@
 #include "src/tint/lang/wgsl/ls/sem_token.h"
 #include "src/tint/lang/wgsl/ls/server.h"
 #include "src/tint/lang/wgsl/ls/utils.h"
+#include "src/tint/lang/wgsl/sem/builtin_enum_expression.h"
 #include "src/tint/lang/wgsl/sem/function_expression.h"
 #include "src/tint/lang/wgsl/sem/type_expression.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
diff --git a/src/tint/lang/wgsl/reader/parser/param_list_test.cc b/src/tint/lang/wgsl/reader/parser/param_list_test.cc
index 65550c7..512bd4a 100644
--- a/src/tint/lang/wgsl/reader/parser/param_list_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/param_list_test.cc
@@ -25,6 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
+#include "src/tint/lang/wgsl/ast/builtin_value_name.h"
 #include "src/tint/lang/wgsl/ast/helper_test.h"
 #include "src/tint/lang/wgsl/reader/parser/helper_test.h"
 
@@ -115,7 +116,7 @@
     auto attrs_0 = e.value[0]->attributes;
     ASSERT_EQ(attrs_0.Length(), 1u);
     EXPECT_TRUE(attrs_0[0]->Is<ast::BuiltinAttribute>());
-    ast::CheckIdentifier(attrs_0[0]->As<ast::BuiltinAttribute>()->builtin, "position");
+    ast::CheckIdentifier(attrs_0[0]->As<ast::BuiltinAttribute>()->builtin->name, "position");
 
     ASSERT_EQ(e.value[0]->source.range.begin.line, 1u);
     ASSERT_EQ(e.value[0]->source.range.begin.column, 20u);
diff --git a/src/tint/lang/wgsl/reader/parser/parser.cc b/src/tint/lang/wgsl/reader/parser/parser.cc
index b85555e..a79d213 100644
--- a/src/tint/lang/wgsl/reader/parser/parser.cc
+++ b/src/tint/lang/wgsl/reader/parser/parser.cc
@@ -3038,6 +3038,19 @@
             break;
     }
 
+    if (attr.value == core::Attribute::kBuiltin) {
+        return expect_paren_block(
+            "builtin attribute", [&]() -> Expect<const ast::BuiltinAttribute*> {
+                auto name = expect_builtin_value_name();
+                if (name.errored) {
+                    return Failure::kErrored;
+                }
+                match(Token::Type::kComma);
+
+                return create<ast::BuiltinAttribute>(t.source(), std::move(name.value));
+            });
+    }
+
     Vector<const ast::Expression*, 2> args;
 
     // Handle no parameter items which should have no parens
@@ -3089,8 +3102,6 @@
             return create<ast::BindingAttribute>(t.source(), args[0]);
         case core::Attribute::kBlendSrc:
             return create<ast::BlendSrcAttribute>(t.source(), args[0]);
-        case core::Attribute::kBuiltin:
-            return create<ast::BuiltinAttribute>(t.source(), args[0]);
         case core::Attribute::kColor:
             return create<ast::ColorAttribute>(t.source(), args[0]);
         case core::Attribute::kCompute:
@@ -3189,6 +3200,17 @@
                        wgsl::kDiagnosticSeverityStrings);
 }
 
+// builtin_value_name :
+// | ident_pattern_token
+Expect<const ast::BuiltinValueName*> Parser::expect_builtin_value_name() {
+    auto name = expect_ident("", "builtin value name");
+    if (name.errored) {
+        return Failure::kErrored;
+    }
+
+    return builder_.BuiltinValueName(name.value);
+}
+
 // diagnostic_control
 // : PAREN_LEFT severity_control_name COMMA diagnostic_rule_name COMMA ? PAREN_RIGHT
 Expect<ast::DiagnosticControl> Parser::expect_diagnostic_control() {
diff --git a/src/tint/lang/wgsl/reader/parser/parser.h b/src/tint/lang/wgsl/reader/parser/parser.h
index 993c544..205c6dd 100644
--- a/src/tint/lang/wgsl/reader/parser/parser.h
+++ b/src/tint/lang/wgsl/reader/parser/parser.h
@@ -683,6 +683,9 @@
     /// Parses a diagnostic_rule_name grammar element.
     /// @return the parsed diagnostic rule name.
     Expect<const ast::DiagnosticRuleName*> expect_diagnostic_rule_name();
+    /// Parses a builtin_value_name grammar element.
+    /// @return the parsed builtin value name.
+    Expect<const ast::BuiltinValueName*> expect_builtin_value_name();
 
     /// Splits a peekable token into to parts filling in the peekable fields.
     /// @param lhs the token to set in the current position
diff --git a/src/tint/lang/wgsl/reader/parser/variable_attribute_list_test.cc b/src/tint/lang/wgsl/reader/parser/variable_attribute_list_test.cc
index c1361b7..c72fa38 100644
--- a/src/tint/lang/wgsl/reader/parser/variable_attribute_list_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/variable_attribute_list_test.cc
@@ -52,7 +52,7 @@
     EXPECT_EQ(exp->value, 4u);
 
     ASSERT_TRUE(attr_1->Is<ast::BuiltinAttribute>());
-    ast::CheckIdentifier(attr_1->As<ast::BuiltinAttribute>()->builtin, "position");
+    ast::CheckIdentifier(attr_1->As<ast::BuiltinAttribute>()->builtin->name, "position");
 }
 
 TEST_F(WGSLParserTest, VariableAttributeList_Invalid) {
diff --git a/src/tint/lang/wgsl/reader/parser/variable_attribute_test.cc b/src/tint/lang/wgsl/reader/parser/variable_attribute_test.cc
index c61f47d..bc930e7 100644
--- a/src/tint/lang/wgsl/reader/parser/variable_attribute_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/variable_attribute_test.cc
@@ -246,7 +246,7 @@
     ASSERT_TRUE(var_attr->Is<ast::BuiltinAttribute>());
 
     auto* builtin = var_attr->As<ast::BuiltinAttribute>();
-    ast::CheckIdentifier(builtin->builtin, str);
+    ast::CheckIdentifier(builtin->builtin->name, str);
 }
 TEST_P(BuiltinTest, Attribute_Builtin_TrailingComma) {
     auto str = tint::ToString(GetParam());
@@ -262,7 +262,7 @@
     ASSERT_TRUE(var_attr->Is<ast::BuiltinAttribute>());
 
     auto* builtin = var_attr->As<ast::BuiltinAttribute>();
-    ast::CheckIdentifier(builtin->builtin, str);
+    ast::CheckIdentifier(builtin->builtin->name, str);
 }
 INSTANTIATE_TEST_SUITE_P(WGSLParserTest,
                          BuiltinTest,
@@ -306,7 +306,7 @@
     EXPECT_TRUE(attr.errored);
     EXPECT_EQ(attr.value, nullptr);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:1: builtin expects 1 argument");
+    EXPECT_EQ(p->error(), "1:9: expected builtin value name");
 }
 
 TEST_F(WGSLParserTest, Attribute_Interpolate_Flat) {
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
index 49570b4..7b6e2c8 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
@@ -87,6 +87,7 @@
 #include "src/tint/lang/wgsl/ast/while_statement.h"
 #include "src/tint/lang/wgsl/ir/builtin_call.h"
 #include "src/tint/lang/wgsl/program/program.h"
+#include "src/tint/lang/wgsl/sem/builtin_enum_expression.h"
 #include "src/tint/lang/wgsl/sem/builtin_fn.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/function.h"
@@ -316,10 +317,7 @@
                     },
                     [&](const ast::InvariantAttribute*) { ir_func->SetReturnInvariant(true); },
                     [&](const ast::BuiltinAttribute* b) {
-                        if (auto* ident_sem =
-                                program_.Sem()
-                                    .Get(b)
-                                    ->As<sem::BuiltinEnumExpression<core::BuiltinValue>>()) {
+                        if (auto* ident_sem = program_.Sem().Get(b)->As<sem::BuiltinAttribute>()) {
                             ir_func->SetReturnBuiltin(ident_sem->Value());
                         } else {
                             TINT_ICE() << "Builtin attribute sem invalid";
@@ -346,10 +344,7 @@
                     },
                     [&](const ast::InvariantAttribute*) { param->SetInvariant(true); },
                     [&](const ast::BuiltinAttribute* b) {
-                        if (auto* ident_sem =
-                                program_.Sem()
-                                    .Get(b)
-                                    ->As<sem::BuiltinEnumExpression<core::BuiltinValue>>()) {
+                        if (auto* ident_sem = program_.Sem().Get(b)->As<sem::BuiltinAttribute>()) {
                             param->SetBuiltin(ident_sem->Value());
                         } else {
                             TINT_ICE() << "Builtin attribute sem invalid";
diff --git a/src/tint/lang/wgsl/resolver/compatibility_mode_test.cc b/src/tint/lang/wgsl/resolver/compatibility_mode_test.cc
index 16793d3..28f52e9 100644
--- a/src/tint/lang/wgsl/resolver/compatibility_mode_test.cc
+++ b/src/tint/lang/wgsl/resolver/compatibility_mode_test.cc
@@ -103,7 +103,8 @@
     Func("main",
          Vector{Param("mask", ty.i32(),
                       Vector{
-                          create<ast::BuiltinAttribute>({}, Expr(Source{{12, 34}}, "sample_mask")),
+                          create<ast::BuiltinAttribute>(
+                              {}, BuiltinValueName(Source{{12, 34}}, "sample_mask")),
                       })},
          ty.void_(), Empty,
          Vector{
@@ -133,7 +134,7 @@
              Stage(ast::PipelineStage::kFragment),
          },
          Vector{
-             create<ast::BuiltinAttribute>({}, Expr(Source{{12, 34}}, "sample_mask")),
+             create<ast::BuiltinAttribute>({}, BuiltinValueName(Source{{12, 34}}, "sample_mask")),
          });
 
     EXPECT_FALSE(r()->Resolve());
@@ -147,14 +148,13 @@
     //   @builtin(sample_mask) mask : u32,
     // }
 
-    Structure(
-        "S",
-        Vector{
-            Member("mask", ty.u32(),
-                   Vector{
-                       create<ast::BuiltinAttribute>({}, Expr(Source{{12, 34}}, "sample_mask")),
-                   }),
-        });
+    Structure("S", Vector{
+                       Member("mask", ty.u32(),
+                              Vector{
+                                  create<ast::BuiltinAttribute>(
+                                      {}, BuiltinValueName(Source{{12, 34}}, "sample_mask")),
+                              }),
+                   });
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
@@ -170,7 +170,8 @@
     Func("main",
          Vector{Param("mask", ty.i32(),
                       Vector{
-                          create<ast::BuiltinAttribute>({}, Expr(Source{{12, 34}}, "sample_index")),
+                          create<ast::BuiltinAttribute>(
+                              {}, BuiltinValueName(Source{{12, 34}}, "sample_index")),
                       })},
          ty.void_(), Empty,
          Vector{
@@ -200,7 +201,7 @@
              Stage(ast::PipelineStage::kFragment),
          },
          Vector{
-             create<ast::BuiltinAttribute>({}, Expr(Source{{12, 34}}, "sample_index")),
+             create<ast::BuiltinAttribute>({}, BuiltinValueName(Source{{12, 34}}, "sample_index")),
          });
 
     EXPECT_FALSE(r()->Resolve());
@@ -214,14 +215,13 @@
     //   @builtin(sample_index) mask : u32,
     // }
 
-    Structure(
-        "S",
-        Vector{
-            Member("mask", ty.u32(),
-                   Vector{
-                       create<ast::BuiltinAttribute>({}, Expr(Source{{12, 34}}, "sample_index")),
-                   }),
-        });
+    Structure("S", Vector{
+                       Member("mask", ty.u32(),
+                              Vector{
+                                  create<ast::BuiltinAttribute>(
+                                      {}, BuiltinValueName(Source{{12, 34}}, "sample_index")),
+                              }),
+                   });
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
diff --git a/src/tint/lang/wgsl/resolver/dependency_graph.cc b/src/tint/lang/wgsl/resolver/dependency_graph.cc
index bffa92e..730845c 100644
--- a/src/tint/lang/wgsl/resolver/dependency_graph.cc
+++ b/src/tint/lang/wgsl/resolver/dependency_graph.cc
@@ -376,7 +376,6 @@
         Switch(
             attr,  //
             [&](const ast::BindingAttribute* binding) { TraverseExpression(binding->expr); },
-            [&](const ast::BuiltinAttribute* builtin) { TraverseExpression(builtin->builtin); },
             [&](const ast::ColorAttribute* color) { TraverseExpression(color->expr); },
             [&](const ast::GroupAttribute* group) { TraverseExpression(group->expr); },
             [&](const ast::IdAttribute* id) { TraverseExpression(id->expr); },
@@ -417,8 +416,6 @@
         kFunction,
         /// Builtin
         kBuiltin,
-        /// Builtin value
-        kBuiltinValue,
         /// Address space
         kAddressSpace,
         /// Texel format
@@ -443,7 +440,6 @@
         std::variant<std::monostate,
                      wgsl::BuiltinFn,
                      core::BuiltinType,
-                     core::BuiltinValue,
                      core::AddressSpace,
                      core::TexelFormat,
                      core::Access,
@@ -465,10 +461,6 @@
                 builtin_ty != core::BuiltinType::kUndefined) {
                 return BuiltinInfo{BuiltinType::kBuiltin, builtin_ty};
             }
-            if (auto builtin_val = core::ParseBuiltinValue(symbol.NameView());
-                builtin_val != core::BuiltinValue::kUndefined) {
-                return BuiltinInfo{BuiltinType::kBuiltinValue, builtin_val};
-            }
             if (auto addr = core::ParseAddressSpace(symbol.NameView());
                 addr != core::AddressSpace::kUndefined) {
                 return BuiltinInfo{BuiltinType::kAddressSpace, addr};
@@ -511,10 +503,6 @@
                     graph_.resolved_identifiers.Add(
                         from, ResolvedIdentifier(builtin_info.Value<core::BuiltinType>()));
                     break;
-                case BuiltinType::kBuiltinValue:
-                    graph_.resolved_identifiers.Add(
-                        from, ResolvedIdentifier(builtin_info.Value<core::BuiltinValue>()));
-                    break;
                 case BuiltinType::kAddressSpace:
                     graph_.resolved_identifiers.Add(
                         from, ResolvedIdentifier(builtin_info.Value<core::AddressSpace>()));
@@ -885,9 +873,6 @@
     if (auto builtin_ty = BuiltinType(); builtin_ty != core::BuiltinType::kUndefined) {
         return "builtin type '" + tint::ToString(builtin_ty) + "'";
     }
-    if (auto builtin_val = BuiltinValue(); builtin_val != core::BuiltinValue::kUndefined) {
-        return "builtin value '" + tint::ToString(builtin_val) + "'";
-    }
     if (auto access = Access(); access != core::Access::kUndefined) {
         return "access '" + tint::ToString(access) + "'";
     }
diff --git a/src/tint/lang/wgsl/resolver/dependency_graph.h b/src/tint/lang/wgsl/resolver/dependency_graph.h
index 1e103d4..80f99cf 100644
--- a/src/tint/lang/wgsl/resolver/dependency_graph.h
+++ b/src/tint/lang/wgsl/resolver/dependency_graph.h
@@ -54,7 +54,6 @@
 /// - core::Access
 /// - core::AddressSpace
 /// - core::BuiltinType
-/// - core::BuiltinValue
 /// - core::InterpolationSampling
 /// - core::InterpolationType
 /// - core::TexelFormat
@@ -123,15 +122,6 @@
         return core::BuiltinType::kUndefined;
     }
 
-    /// @return the builtin value if the ResolvedIdentifier holds core::BuiltinValue, otherwise
-    /// core::BuiltinValue::kUndefined
-    core::BuiltinValue BuiltinValue() const {
-        if (auto n = std::get_if<core::BuiltinValue>(&value_)) {
-            return *n;
-        }
-        return core::BuiltinValue::kUndefined;
-    }
-
     /// @return the texel format if the ResolvedIdentifier holds type::InterpolationSampling,
     /// otherwise type::InterpolationSampling::kUndefined
     core::InterpolationSampling InterpolationSampling() const {
@@ -186,7 +176,6 @@
                  core::Access,
                  core::AddressSpace,
                  core::BuiltinType,
-                 core::BuiltinValue,
                  core::InterpolationSampling,
                  core::InterpolationType,
                  core::TexelFormat>
diff --git a/src/tint/lang/wgsl/resolver/dependency_graph_test.cc b/src/tint/lang/wgsl/resolver/dependency_graph_test.cc
index f64306a..a145a93 100644
--- a/src/tint/lang/wgsl/resolver/dependency_graph_test.cc
+++ b/src/tint/lang/wgsl/resolver/dependency_graph_test.cc
@@ -1349,46 +1349,6 @@
 }  // namespace resolve_to_address_space
 
 ////////////////////////////////////////////////////////////////////////////////
-// Resolve to core::BuiltinValue tests
-////////////////////////////////////////////////////////////////////////////////
-namespace resolve_to_builtin_value {
-
-using ResolverDependencyGraphResolveToBuiltinValue =
-    ResolverDependencyGraphTestWithParam<std::tuple<SymbolUseKind, std::string_view>>;
-
-TEST_P(ResolverDependencyGraphResolveToBuiltinValue, Resolve) {
-    const auto use = std::get<0>(GetParam());
-    const auto name = std::get<1>(GetParam());
-    const auto symbol = Symbols().New(name);
-
-    SymbolTestHelper helper(this);
-    auto* ident = helper.Add(use, symbol);
-    helper.Build();
-
-    auto graph = Build();
-    auto resolved = graph.resolved_identifiers.Get(ident);
-    ASSERT_TRUE(resolved);
-    EXPECT_EQ(resolved->BuiltinValue(), core::ParseBuiltinValue(name)) << resolved->String();
-}
-
-INSTANTIATE_TEST_SUITE_P(Types,
-                         ResolverDependencyGraphResolveToBuiltinValue,
-                         testing::Combine(testing::ValuesIn(kTypeUseKinds),
-                                          testing::ValuesIn(core::kBuiltinValueStrings)));
-
-INSTANTIATE_TEST_SUITE_P(Values,
-                         ResolverDependencyGraphResolveToBuiltinValue,
-                         testing::Combine(testing::ValuesIn(kValueUseKinds),
-                                          testing::ValuesIn(core::kBuiltinValueStrings)));
-
-INSTANTIATE_TEST_SUITE_P(Functions,
-                         ResolverDependencyGraphResolveToBuiltinValue,
-                         testing::Combine(testing::ValuesIn(kFuncUseKinds),
-                                          testing::ValuesIn(core::kBuiltinValueStrings)));
-
-}  // namespace resolve_to_builtin_value
-
-////////////////////////////////////////////////////////////////////////////////
 // Resolve to core::InterpolationSampling tests
 ////////////////////////////////////////////////////////////////////////////////
 namespace resolve_to_interpolation_sampling {
@@ -1704,7 +1664,6 @@
                    Vector{
                        Location(V),  // Parameter attributes
                        Color(V),
-                       Builtin(V),
                        Interpolate(V),
                        Interpolate(V, V),
                    }),
diff --git a/src/tint/lang/wgsl/resolver/expression_kind_test.cc b/src/tint/lang/wgsl/resolver/expression_kind_test.cc
index 1334b8b..5a60449 100644
--- a/src/tint/lang/wgsl/resolver/expression_kind_test.cc
+++ b/src/tint/lang/wgsl/resolver/expression_kind_test.cc
@@ -42,7 +42,6 @@
     kAddressSpace,
     kBuiltinFunction,
     kBuiltinType,
-    kBuiltinValue,
     kFunction,
     kInterpolationSampling,
     kInterpolationType,
@@ -63,8 +62,6 @@
             return out << "Def::kBuiltinFunction";
         case Def::kBuiltinType:
             return out << "Def::kBuiltinType";
-        case Def::kBuiltinValue:
-            return out << "Def::kBuiltinValue";
         case Def::kFunction:
             return out << "Def::kFunction";
         case Def::kInterpolationSampling:
@@ -89,7 +86,6 @@
     kAccess,
     kAddressSpace,
     kBinaryOp,
-    kBuiltinValue,
     kCallExpr,
     kCallStmt,
     kFunctionReturnType,
@@ -110,8 +106,6 @@
             return out << "Use::kAddressSpace";
         case Use::kBinaryOp:
             return out << "Use::kBinaryOp";
-        case Use::kBuiltinValue:
-            return out << "Use::kBuiltinValue";
         case Use::kCallExpr:
             return out << "Use::kCallExpr";
         case Use::kCallStmt:
@@ -202,16 +196,6 @@
             };
             break;
         }
-        case Def::kBuiltinValue: {
-            sym = Sym("position");
-            check_expr = [](const sem::Expression* expr) {
-                ASSERT_NE(expr, nullptr);
-                auto* enum_expr = expr->As<sem::BuiltinEnumExpression<core::BuiltinValue>>();
-                ASSERT_NE(enum_expr, nullptr);
-                EXPECT_EQ(enum_expr->Value(), core::BuiltinValue::kPosition);
-            };
-            break;
-        }
         case Def::kFunction: {
             sym = Sym("FUNCTION");
             auto* fn = Func(kDefSource, sym, tint::Empty, ty.i32(), Return(1_i));
@@ -303,7 +287,8 @@
         }
     }
 
-    auto* expr = Expr(Ident(kUseSource, sym));
+    auto* ident = Ident(kUseSource, sym);
+    auto* expr = Expr(ident);
     switch (GetParam().use) {
         case Use::kAccess:
             GlobalVar("v", ty("texture_storage_2d", "rgba8unorm", expr), Group(0_u), Binding(0_u));
@@ -321,10 +306,6 @@
         case Use::kBinaryOp:
             fn_stmts.Push(Decl(Var("v", Mul(1_a, expr))));
             break;
-        case Use::kBuiltinValue:
-            Func(Symbols().New(), Vector{Param("p", ty.vec4<f32>(), Vector{Builtin(expr)})},
-                 ty.void_(), tint::Empty, Vector{Stage(ast::PipelineStage::kFragment)});
-            break;
         case Use::kFunctionReturnType:
             Func(Symbols().New(), tint::Empty, ty(expr), Return(Call(sym)));
             break;
@@ -387,8 +368,6 @@
         {Def::kAccess, Use::kAddressSpace,
          R"(5:6 error: cannot use access 'write' as address space)"},
         {Def::kAccess, Use::kBinaryOp, R"(5:6 error: cannot use access 'write' as value)"},
-        {Def::kAccess, Use::kBuiltinValue,
-         R"(5:6 error: cannot use access 'write' as builtin value)"},
         {Def::kAccess, Use::kCallExpr, R"(5:6 error: cannot use access 'write' as call target)"},
         {Def::kAccess, Use::kCallStmt, R"(5:6 error: cannot use access 'write' as call target)"},
         {Def::kAccess, Use::kFunctionReturnType, R"(5:6 error: cannot use access 'write' as type)"},
@@ -408,8 +387,6 @@
         {Def::kAddressSpace, Use::kAddressSpace, kPass},
         {Def::kAddressSpace, Use::kBinaryOp,
          R"(5:6 error: cannot use address space 'workgroup' as value)"},
-        {Def::kAddressSpace, Use::kBuiltinValue,
-         R"(5:6 error: cannot use address space 'workgroup' as builtin value)"},
         {Def::kAddressSpace, Use::kCallExpr,
          R"(5:6 error: cannot use address space 'workgroup' as call target)"},
         {Def::kAddressSpace, Use::kCallStmt,
@@ -438,8 +415,6 @@
         {Def::kBuiltinFunction, Use::kBinaryOp,
          R"(5:6 error: cannot use builtin function 'workgroupBarrier' as value
 7:8 note: are you missing '()'?)"},
-        {Def::kBuiltinFunction, Use::kBuiltinValue,
-         R"(5:6 error: cannot use builtin function 'workgroupBarrier' as builtin value)"},
         {Def::kBuiltinFunction, Use::kCallStmt, kPass},
         {Def::kBuiltinFunction, Use::kFunctionReturnType,
          R"(5:6 error: cannot use builtin function 'workgroupBarrier' as type)"},
@@ -466,8 +441,6 @@
         {Def::kBuiltinType, Use::kBinaryOp,
          R"(5:6 error: cannot use type 'vec4<f32>' as value
 7:8 note: are you missing '()'?)"},
-        {Def::kBuiltinType, Use::kBuiltinValue,
-         R"(5:6 error: cannot use type 'vec4<f32>' as builtin value)"},
         {Def::kBuiltinType, Use::kCallExpr, kPass},
         {Def::kBuiltinType, Use::kFunctionReturnType, kPass},
         {Def::kBuiltinType, Use::kInterpolationSampling,
@@ -485,34 +458,6 @@
          R"(5:6 error: cannot use type 'vec4<f32>' as value
 7:8 note: are you missing '()'?)"},
 
-        {Def::kBuiltinValue, Use::kAccess,
-         R"(5:6 error: cannot use builtin value 'position' as access)"},
-        {Def::kBuiltinValue, Use::kAddressSpace,
-         R"(5:6 error: cannot use builtin value 'position' as address space)"},
-        {Def::kBuiltinValue, Use::kBinaryOp,
-         R"(5:6 error: cannot use builtin value 'position' as value)"},
-        {Def::kBuiltinValue, Use::kBuiltinValue, kPass},
-        {Def::kBuiltinValue, Use::kCallStmt,
-         R"(5:6 error: cannot use builtin value 'position' as call target)"},
-        {Def::kBuiltinValue, Use::kCallExpr,
-         R"(5:6 error: cannot use builtin value 'position' as call target)"},
-        {Def::kBuiltinValue, Use::kFunctionReturnType,
-         R"(5:6 error: cannot use builtin value 'position' as type)"},
-        {Def::kBuiltinValue, Use::kInterpolationSampling,
-         R"(5:6 error: cannot use builtin value 'position' as interpolation sampling)"},
-        {Def::kBuiltinValue, Use::kInterpolationType,
-         R"(5:6 error: cannot use builtin value 'position' as interpolation type)"},
-        {Def::kBuiltinValue, Use::kMemberType,
-         R"(5:6 error: cannot use builtin value 'position' as type)"},
-        {Def::kBuiltinValue, Use::kTexelFormat,
-         R"(5:6 error: cannot use builtin value 'position' as texel format)"},
-        {Def::kBuiltinValue, Use::kValueExpression,
-         R"(5:6 error: cannot use builtin value 'position' as value)"},
-        {Def::kBuiltinValue, Use::kVariableType,
-         R"(5:6 error: cannot use builtin value 'position' as type)"},
-        {Def::kBuiltinValue, Use::kUnaryOp,
-         R"(5:6 error: cannot use builtin value 'position' as value)"},
-
         {Def::kFunction, Use::kAccess, R"(5:6 error: cannot use function 'FUNCTION' as access
 1:2 note: function 'FUNCTION' declared here)"},
         {Def::kFunction, Use::kAddressSpace,
@@ -521,9 +466,6 @@
         {Def::kFunction, Use::kBinaryOp, R"(5:6 error: cannot use function 'FUNCTION' as value
 1:2 note: function 'FUNCTION' declared here
 7:8 note: are you missing '()'?)"},
-        {Def::kFunction, Use::kBuiltinValue,
-         R"(5:6 error: cannot use function 'FUNCTION' as builtin value
-1:2 note: function 'FUNCTION' declared here)"},
         {Def::kFunction, Use::kCallExpr, kPass},
         {Def::kFunction, Use::kCallStmt, kPass},
         {Def::kFunction, Use::kFunctionReturnType,
@@ -558,8 +500,6 @@
          R"(5:6 error: cannot use interpolation sampling 'center' as address space)"},
         {Def::kInterpolationSampling, Use::kBinaryOp,
          R"(5:6 error: cannot use interpolation sampling 'center' as value)"},
-        {Def::kInterpolationSampling, Use::kBuiltinValue,
-         R"(5:6 error: cannot use interpolation sampling 'center' as builtin value)"},
         {Def::kInterpolationSampling, Use::kCallStmt,
          R"(5:6 error: cannot use interpolation sampling 'center' as call target)"},
         {Def::kInterpolationSampling, Use::kCallExpr,
@@ -586,8 +526,6 @@
          R"(5:6 error: cannot use interpolation type 'linear' as address space)"},
         {Def::kInterpolationType, Use::kBinaryOp,
          R"(5:6 error: cannot use interpolation type 'linear' as value)"},
-        {Def::kInterpolationType, Use::kBuiltinValue,
-         R"(5:6 error: cannot use interpolation type 'linear' as builtin value)"},
         {Def::kInterpolationType, Use::kCallStmt,
          R"(5:6 error: cannot use interpolation type 'linear' as call target)"},
         {Def::kInterpolationType, Use::kCallExpr,
@@ -629,9 +567,6 @@
         {Def::kStruct, Use::kBinaryOp, R"(5:6 error: cannot use type 'STRUCT' as value
 1:2 note: 'struct STRUCT' declared here
 7:8 note: are you missing '()'?)"},
-        {Def::kStruct, Use::kBuiltinValue,
-         R"(5:6 error: cannot use type 'STRUCT' as builtin value
-1:2 note: 'struct STRUCT' declared here)"},
         {Def::kStruct, Use::kFunctionReturnType, kPass},
         {Def::kStruct, Use::kInterpolationSampling,
          R"(5:6 error: cannot use type 'STRUCT' as interpolation sampling
@@ -658,8 +593,6 @@
          R"(5:6 error: cannot use texel format 'rgba8unorm' as address space)"},
         {Def::kTexelFormat, Use::kBinaryOp,
          R"(5:6 error: cannot use texel format 'rgba8unorm' as value)"},
-        {Def::kTexelFormat, Use::kBuiltinValue,
-         R"(5:6 error: cannot use texel format 'rgba8unorm' as builtin value)"},
         {Def::kTexelFormat, Use::kCallExpr,
          R"(5:6 error: cannot use texel format 'rgba8unorm' as call target)"},
         {Def::kTexelFormat, Use::kCallStmt,
@@ -686,8 +619,6 @@
         {Def::kTypeAlias, Use::kBinaryOp,
          R"(5:6 error: cannot use type 'i32' as value
 7:8 note: are you missing '()'?)"},
-        {Def::kTypeAlias, Use::kBuiltinValue,
-         R"(5:6 error: cannot use type 'i32' as builtin value)"},
         {Def::kTypeAlias, Use::kCallExpr, kPass},
         {Def::kTypeAlias, Use::kFunctionReturnType, kPass},
         {Def::kTypeAlias, Use::kInterpolationSampling,
@@ -710,9 +641,6 @@
          R"(5:6 error: cannot use 'const VARIABLE' as address space
 1:2 note: 'const VARIABLE' declared here)"},
         {Def::kVariable, Use::kBinaryOp, kPass},
-        {Def::kVariable, Use::kBuiltinValue,
-         R"(5:6 error: cannot use 'const VARIABLE' as builtin value
-1:2 note: 'const VARIABLE' declared here)"},
         {Def::kVariable, Use::kCallStmt,
          R"(5:6 error: cannot use 'const VARIABLE' as call target
 1:2 note: 'const VARIABLE' declared here)"},
diff --git a/src/tint/lang/wgsl/resolver/ptr_ref_validation_test.cc b/src/tint/lang/wgsl/resolver/ptr_ref_validation_test.cc
index e7b05da..a25b36b 100644
--- a/src/tint/lang/wgsl/resolver/ptr_ref_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/ptr_ref_validation_test.cc
@@ -182,13 +182,13 @@
     EXPECT_EQ(r()->error(), R"(12:34 error: cannot use address space 'uniform' as value)");
 }
 
-TEST_F(ResolverPtrRefValidationTest, AddressOfBuiltinValue) {
+TEST_F(ResolverPtrRefValidationTest, AddressOfUnresolvedValue) {
     // &position
     auto* expr = AddressOf(Expr(Source{{12, 34}}, "position"));
     WrapInFunction(expr);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: cannot use builtin value 'position' as value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: unresolved value 'position')");
 }
 
 TEST_F(ResolverPtrRefValidationTest, AddressOfInterpolationSampling) {
diff --git a/src/tint/lang/wgsl/resolver/resolver.cc b/src/tint/lang/wgsl/resolver/resolver.cc
index 2362908..0250d5f 100644
--- a/src/tint/lang/wgsl/resolver/resolver.cc
+++ b/src/tint/lang/wgsl/resolver/resolver.cc
@@ -58,6 +58,7 @@
 #include "src/tint/lang/wgsl/ast/assignment_statement.h"
 #include "src/tint/lang/wgsl/ast/attribute.h"
 #include "src/tint/lang/wgsl/ast/break_statement.h"
+#include "src/tint/lang/wgsl/ast/builtin_value_name.h"
 #include "src/tint/lang/wgsl/ast/call_statement.h"
 #include "src/tint/lang/wgsl/ast/continue_statement.h"
 #include "src/tint/lang/wgsl/ast/disable_validation_attribute.h"
@@ -83,6 +84,7 @@
 #include "src/tint/lang/wgsl/resolver/unresolved_identifier.h"
 #include "src/tint/lang/wgsl/sem/array.h"
 #include "src/tint/lang/wgsl/sem/break_if_statement.h"
+#include "src/tint/lang/wgsl/sem/builtin_attribute.h"
 #include "src/tint/lang/wgsl/sem/builtin_enum_expression.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/for_loop_statement.h"
@@ -1672,11 +1674,6 @@
     return address_space_expr;
 }
 
-sem::BuiltinEnumExpression<core::BuiltinValue>* Resolver::BuiltinValueExpression(
-    const ast::Expression* expr) {
-    return sem_.AsBuiltinValue(Expression(expr));
-}
-
 sem::BuiltinEnumExpression<core::TexelFormat>* Resolver::TexelFormatExpression(
     const ast::Expression* expr) {
     return sem_.AsTexelFormat(Expression(expr));
@@ -3405,13 +3402,6 @@
                    : nullptr;
     }
 
-    if (auto builtin = resolved->BuiltinValue(); builtin != core::BuiltinValue::kUndefined) {
-        return CheckNotTemplated("builtin value", ident)
-                   ? b.create<sem::BuiltinEnumExpression<core::BuiltinValue>>(
-                         expr, current_statement_, builtin)
-                   : nullptr;
-    }
-
     if (auto i_smpl = resolved->InterpolationSampling();
         i_smpl != core::InterpolationSampling::kUndefined) {
         return CheckNotTemplated("interpolation sampling", ident)
@@ -4015,14 +4005,25 @@
 
 tint::Result<tint::core::BuiltinValue> Resolver::BuiltinAttribute(
     const ast::BuiltinAttribute* attr) {
-    auto* builtin_expr = BuiltinValueExpression(attr->builtin);
-    if (!builtin_expr) {
+    const ast::BuiltinValueName* builtin_val = attr->builtin;
+    Mark(builtin_val);
+
+    const ast::Identifier* ident = builtin_val->name;
+    if (!TINT_LIKELY(CheckNotTemplated("builtin value", ident))) {
         return Failure{};
     }
-    // Apply the resolved tint::sem::BuiltinEnumExpression<tint::core::BuiltinValue> to the
-    // attribute.
-    b.Sem().Add(attr, builtin_expr);
-    return builtin_expr->Value();
+    Mark(ident);
+
+    core::BuiltinValue builtin = core::ParseBuiltinValue(ident->symbol.NameView());
+    if (builtin == core::BuiltinValue::kUndefined) {
+        sem_.ErrorUnexpectedIdent(ident, "builtin value", core::kBuiltinValueStrings);
+        return Failure{};
+    }
+
+    auto* sem = b.create<sem::BuiltinAttribute>(attr, builtin);
+    // Apply the resolved tint::sem::BuiltinAttribute to the attribute.
+    b.Sem().Add(attr, sem);
+    return builtin;
 }
 
 bool Resolver::DiagnosticAttribute(const ast::DiagnosticAttribute* attr) {
diff --git a/src/tint/lang/wgsl/resolver/resolver.h b/src/tint/lang/wgsl/resolver/resolver.h
index e57b6ad..bd09492 100644
--- a/src/tint/lang/wgsl/resolver/resolver.h
+++ b/src/tint/lang/wgsl/resolver/resolver.h
@@ -254,13 +254,6 @@
         const ast::Expression* expr);
 
     /// @returns the call of Expression() cast to a
-    /// sem::BuiltinEnumExpression<core::BuiltinValue>. If the sem::Expression is not a
-    /// sem::BuiltinEnumExpression<core::BuiltinValue>, then an error diagnostic is raised and
-    /// nullptr is returned.
-    sem::BuiltinEnumExpression<core::BuiltinValue>* BuiltinValueExpression(
-        const ast::Expression* expr);
-
-    /// @returns the call of Expression() cast to a
     /// sem::BuiltinEnumExpression<core::type::TexelFormat>. If the sem::Expression is not a
     /// sem::BuiltinEnumExpression<core::type::TexelFormat>, then an error diagnostic is raised and
     /// nullptr is returned.
diff --git a/src/tint/lang/wgsl/resolver/sem_helper.cc b/src/tint/lang/wgsl/resolver/sem_helper.cc
index 1fe3934..19f8657 100644
--- a/src/tint/lang/wgsl/resolver/sem_helper.cc
+++ b/src/tint/lang/wgsl/resolver/sem_helper.cc
@@ -117,9 +117,6 @@
         [&](const sem::BuiltinEnumExpression<core::AddressSpace>* addr) {
             text << "address space " << style::Enum(addr->Value());
         },
-        [&](const sem::BuiltinEnumExpression<core::BuiltinValue>* builtin) {
-            text << "builtin value " << style::Enum(builtin->Value());
-        },
         [&](const sem::BuiltinEnumExpression<core::InterpolationSampling>* fmt) {
             text << "interpolation sampling " << style::Enum(fmt->Value());
         },
@@ -138,28 +135,33 @@
     return text;
 }
 
+void SemHelper::ErrorUnexpectedIdent(
+    const ast::Identifier* ident,
+    std::string_view wanted,
+    tint::Slice<const std::string_view> suggestions /* = Empty */) const {
+    auto name = ident->symbol.NameView();
+    AddError(ident->source) << "unresolved " << wanted << " " << style::Code(name);
+    if (!suggestions.IsEmpty()) {
+        // Filter out suggestions that have a leading underscore.
+        Vector<std::string_view, 8> filtered;
+        for (auto str : suggestions) {
+            if (str[0] != '_') {
+                filtered.Push(str);
+            }
+        }
+        auto& note = AddNote(ident->source);
+        SuggestAlternativeOptions opts;
+        opts.alternatives_style = style::Enum;
+        SuggestAlternatives(name, filtered.Slice(), note.message, opts);
+    }
+}
+
 void SemHelper::ErrorUnexpectedExprKind(
     const sem::Expression* expr,
     std::string_view wanted,
     tint::Slice<const std::string_view> suggestions /* = Empty */) const {
     if (auto* ui = expr->As<UnresolvedIdentifier>()) {
-        auto* ident = ui->Identifier();
-        auto name = ident->identifier->symbol.NameView();
-        AddError(ident->source) << "unresolved " << wanted << " " << style::Code(name);
-        if (!suggestions.IsEmpty()) {
-            // Filter out suggestions that have a leading underscore.
-            Vector<std::string_view, 8> filtered;
-            for (auto str : suggestions) {
-                if (str[0] != '_') {
-                    filtered.Push(str);
-                }
-            }
-            auto& note = AddNote(ident->source);
-            SuggestAlternativeOptions opts;
-            opts.alternatives_style = style::Enum;
-            SuggestAlternatives(name, filtered.Slice(), note.message, opts);
-        }
-        return;
+        return ErrorUnexpectedIdent(ui->Identifier()->identifier, wanted, suggestions);
     }
 
     AddError(expr->Declaration()->source) << "cannot use " << Describe(expr) << " as " << wanted;
diff --git a/src/tint/lang/wgsl/resolver/sem_helper.h b/src/tint/lang/wgsl/resolver/sem_helper.h
index fd7b700..f8e123c 100644
--- a/src/tint/lang/wgsl/resolver/sem_helper.h
+++ b/src/tint/lang/wgsl/resolver/sem_helper.h
@@ -154,21 +154,6 @@
 
     /// @param expr the semantic node
     /// @returns nullptr if @p expr is nullptr, or @p expr cast to
-    /// sem::BuiltinEnumExpression<core::BuiltinValue> if the cast is successful, otherwise an
-    /// error diagnostic is raised.
-    sem::BuiltinEnumExpression<core::BuiltinValue>* AsBuiltinValue(sem::Expression* expr) const {
-        if (TINT_LIKELY(expr)) {
-            auto* enum_expr = expr->As<sem::BuiltinEnumExpression<core::BuiltinValue>>();
-            if (TINT_LIKELY(enum_expr)) {
-                return enum_expr;
-            }
-            ErrorUnexpectedExprKind(expr, "builtin value", core::kBuiltinValueStrings);
-        }
-        return nullptr;
-    }
-
-    /// @param expr the semantic node
-    /// @returns nullptr if @p expr is nullptr, or @p expr cast to
     /// sem::BuiltinEnumExpression<core::type::TexelFormat> if the cast is successful, otherwise an
     /// error diagnostic is raised.
     sem::BuiltinEnumExpression<core::TexelFormat>* AsTexelFormat(sem::Expression* expr) const {
@@ -273,6 +258,14 @@
     /// @param expr the expression
     void ErrorExpectedValueExpr(const sem::Expression* expr) const;
 
+    /// Raises an error diagnostic that the identifier @p got was not of the kind @p wanted.
+    /// @param ident the identifier
+    /// @param wanted the expected identifier kind
+    /// @param suggestions suggested valid identifiers
+    void ErrorUnexpectedIdent(const ast::Identifier* ident,
+                              std::string_view wanted,
+                              tint::Slice<const std::string_view> suggestions = Empty) const;
+
     /// Raises an error diagnostic that the expression @p got was not of the kind @p wanted.
     /// @param expr the expression
     /// @param wanted the expected expression kind
diff --git a/src/tint/lang/wgsl/resolver/unresolved_identifier_test.cc b/src/tint/lang/wgsl/resolver/unresolved_identifier_test.cc
index 98aa12d..c87cd04 100644
--- a/src/tint/lang/wgsl/resolver/unresolved_identifier_test.cc
+++ b/src/tint/lang/wgsl/resolver/unresolved_identifier_test.cc
@@ -55,7 +55,7 @@
 }
 
 TEST_F(ResolverUnresolvedIdentifierSuggestions, BuiltinValue) {
-    Func("f", Vector{Param("p", ty.i32(), Vector{Builtin(Expr(Source{{12, 34}}, "positon"))})},
+    Func("f", Vector{Param("p", ty.i32(), Vector{Builtin(Ident(Source{{12, 34}}, "positon"))})},
          ty.void_(), tint::Empty, Vector{Stage(ast::PipelineStage::kVertex)});
 
     EXPECT_FALSE(r()->Resolve());
diff --git a/src/tint/lang/wgsl/sem/BUILD.bazel b/src/tint/lang/wgsl/sem/BUILD.bazel
index 37b8323..2407e96 100644
--- a/src/tint/lang/wgsl/sem/BUILD.bazel
+++ b/src/tint/lang/wgsl/sem/BUILD.bazel
@@ -45,6 +45,7 @@
     "behavior.cc",
     "block_statement.cc",
     "break_if_statement.cc",
+    "builtin_attribute.cc",
     "builtin_enum_expression.cc",
     "builtin_fn.cc",
     "call.cc",
@@ -79,6 +80,7 @@
     "behavior.h",
     "block_statement.h",
     "break_if_statement.h",
+    "builtin_attribute.h",
     "builtin_enum_expression.h",
     "builtin_fn.h",
     "call.h",
diff --git a/src/tint/lang/wgsl/sem/BUILD.cmake b/src/tint/lang/wgsl/sem/BUILD.cmake
index 6120b90..c83602b 100644
--- a/src/tint/lang/wgsl/sem/BUILD.cmake
+++ b/src/tint/lang/wgsl/sem/BUILD.cmake
@@ -51,6 +51,8 @@
   lang/wgsl/sem/block_statement.h
   lang/wgsl/sem/break_if_statement.cc
   lang/wgsl/sem/break_if_statement.h
+  lang/wgsl/sem/builtin_attribute.cc
+  lang/wgsl/sem/builtin_attribute.h
   lang/wgsl/sem/builtin_enum_expression.cc
   lang/wgsl/sem/builtin_enum_expression.h
   lang/wgsl/sem/builtin_fn.cc
diff --git a/src/tint/lang/wgsl/sem/BUILD.gn b/src/tint/lang/wgsl/sem/BUILD.gn
index e6b2b06..4131c9a 100644
--- a/src/tint/lang/wgsl/sem/BUILD.gn
+++ b/src/tint/lang/wgsl/sem/BUILD.gn
@@ -56,6 +56,8 @@
     "block_statement.h",
     "break_if_statement.cc",
     "break_if_statement.h",
+    "builtin_attribute.cc",
+    "builtin_attribute.h",
     "builtin_enum_expression.cc",
     "builtin_enum_expression.h",
     "builtin_fn.cc",
diff --git a/src/tint/lang/wgsl/sem/builtin_attribute.cc b/src/tint/lang/wgsl/sem/builtin_attribute.cc
new file mode 100644
index 0000000..d6e3784
--- /dev/null
+++ b/src/tint/lang/wgsl/sem/builtin_attribute.cc
@@ -0,0 +1,48 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/sem/builtin_attribute.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::sem::BuiltinAttribute);
+
+namespace tint::sem {
+
+BuiltinAttribute::BuiltinAttribute(const ast::BuiltinAttribute* declaration,
+                                   const core::BuiltinValue value)
+    : Base(), declaration_(declaration), value_(value) {}
+
+BuiltinAttribute::~BuiltinAttribute() = default;
+
+const ast::BuiltinAttribute* BuiltinAttribute::Declaration() const {
+    return declaration_;
+}
+
+core::BuiltinValue BuiltinAttribute::Value() const {
+    return value_;
+}
+
+}  // namespace tint::sem
diff --git a/src/tint/lang/wgsl/sem/builtin_attribute.h b/src/tint/lang/wgsl/sem/builtin_attribute.h
new file mode 100644
index 0000000..4906cf8
--- /dev/null
+++ b/src/tint/lang/wgsl/sem/builtin_attribute.h
@@ -0,0 +1,62 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_WGSL_SEM_BUILTIN_ATTRIBUTE_H_
+#define SRC_TINT_LANG_WGSL_SEM_BUILTIN_ATTRIBUTE_H_
+
+#include "src/tint/lang/core/builtin_value.h"
+#include "src/tint/lang/wgsl/ast/builtin_attribute.h"
+#include "src/tint/lang/wgsl/sem/expression.h"
+
+namespace tint::sem {
+
+/// BuiltinAttribute holds the semantic information for ast nodes that resolve to a
+/// builtin value.
+class BuiltinAttribute : public Castable<BuiltinAttribute, Node> {
+  public:
+    /// Constructor
+    /// @param declaration the AST node
+    /// @param value the builtin value
+    BuiltinAttribute(const ast::BuiltinAttribute* declaration, core::BuiltinValue value);
+
+    /// Destructor
+    ~BuiltinAttribute() override;
+
+    /// @returns the case selector declaration
+    const ast::BuiltinAttribute* Declaration() const;
+
+    /// @return the enumerator value
+    core::BuiltinValue Value() const;
+
+  private:
+    const ast::BuiltinAttribute* declaration_;
+    const core::BuiltinValue value_;
+};
+
+}  // namespace tint::sem
+
+#endif  // SRC_TINT_LANG_WGSL_SEM_BUILTIN_ATTRIBUTE_H_
diff --git a/src/tint/lang/wgsl/sem/type_mappings.h b/src/tint/lang/wgsl/sem/type_mappings.h
index ec3f852..9bb8f59 100644
--- a/src/tint/lang/wgsl/sem/type_mappings.h
+++ b/src/tint/lang/wgsl/sem/type_mappings.h
@@ -30,7 +30,7 @@
 
 #include <type_traits>
 
-#include "src/tint/lang/wgsl/sem/builtin_enum_expression.h"
+#include "src/tint/lang/wgsl/sem/builtin_attribute.h"
 
 // Forward declarations
 namespace tint {
@@ -95,7 +95,7 @@
 struct TypeMappings {
     //! @cond Doxygen_Suppress
     BlockStatement* operator()(ast::BlockStatement*);
-    BuiltinEnumExpression<core::BuiltinValue>* operator()(ast::BuiltinAttribute*);
+    BuiltinAttribute* operator()(ast::BuiltinAttribute*);
     CastableBase* operator()(ast::Node*);
     Expression* operator()(ast::Expression*);
     ForLoopStatement* operator()(ast::ForLoopStatement*);
diff --git a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
index 62f9e82..0f3f77e 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
@@ -38,6 +38,7 @@
 #include "src/tint/lang/wgsl/ast/bool_literal_expression.h"
 #include "src/tint/lang/wgsl/ast/break_if_statement.h"
 #include "src/tint/lang/wgsl/ast/break_statement.h"
+#include "src/tint/lang/wgsl/ast/builtin_value_name.h"
 #include "src/tint/lang/wgsl/ast/call_expression.h"
 #include "src/tint/lang/wgsl/ast/call_statement.h"
 #include "src/tint/lang/wgsl/ast/color_attribute.h"
@@ -508,7 +509,7 @@
             },
             [&](const ast::BuiltinAttribute* builtin) {
                 out << "builtin(";
-                EmitExpression(out, builtin->builtin);
+                out << builtin->builtin->String();
                 out << ")";
             },
             [&](const ast::DiagnosticAttribute* diagnostic) {
diff --git a/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.cc b/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.cc
index 299dd88..f62759b 100644
--- a/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.cc
+++ b/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.cc
@@ -35,6 +35,7 @@
 #include "src/tint/lang/wgsl/ast/bool_literal_expression.h"
 #include "src/tint/lang/wgsl/ast/break_if_statement.h"
 #include "src/tint/lang/wgsl/ast/break_statement.h"
+#include "src/tint/lang/wgsl/ast/builtin_value_name.h"
 #include "src/tint/lang/wgsl/ast/call_expression.h"
 #include "src/tint/lang/wgsl/ast/call_statement.h"
 #include "src/tint/lang/wgsl/ast/compound_assignment_statement.h"
@@ -552,12 +553,7 @@
                 Line() << "]";
             },
             [&](const ast::BuiltinAttribute* builtin) {
-                Line() << "BuiltinAttribute [";
-                {
-                    ScopedIndent ba(this);
-                    EmitExpression(builtin->builtin);
-                }
-                Line() << "]";
+                Line() << "BuiltinAttribute [" << builtin->builtin->String() << "]";
             },
             [&](const ast::DiagnosticAttribute* diagnostic) {
                 EmitDiagnosticControl(diagnostic->control);
diff --git a/webgpu-cts/expectations.txt b/webgpu-cts/expectations.txt
index ebf3079..bf79c2c 100644
--- a/webgpu-cts/expectations.txt
+++ b/webgpu-cts/expectations.txt
@@ -1602,7 +1602,6 @@
 crbug.com/42251274 [ nvidia-0x2184 ubuntu ] webgpu:shader,validation,expression,binary,div_rem:scalar_vector_out_of_range:* [ Failure ]
 crbug.com/42251274 [ nvidia-0x2184 webgpu-dxc-disabled win10 ] webgpu:shader,validation,expression,binary,div_rem:scalar_vector_out_of_range:* [ Failure ]
 crbug.com/42251274 [ nvidia-0x2184 webgpu-dxc-enabled win10 ] webgpu:shader,validation,expression,binary,div_rem:scalar_vector_out_of_range:* [ Failure ]
-crbug.com/tint/2219 webgpu:shader,validation,decl,context_dependent_resolution:builtin_value_names:* [ Failure ]
 crbug.com/tint/2219 webgpu:shader,validation,decl,context_dependent_resolution:interpolation_sampling_names:* [ Failure ]
 crbug.com/tint/2219 webgpu:shader,validation,decl,context_dependent_resolution:interpolation_type_names:* [ Failure ]
 crbug.com/tint/2225 webgpu:shader,execution,expression,unary,i32_conversion:abstract_float:* [ Failure ]