Add `SuggestAlternatives` to attributes.

This CL adds an `attribute` enum into intrinsics.def and updates the
WGSL parser to use the enum parser and SuggestAlternatives.

Bug: tint:1831
Change-Id: I33b3e6bbf092282d9b1f86a1080e69940f55ff68
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/121280
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Dan Sinclair <dsinclair@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 4bcfdfd..a93678f 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -701,6 +701,8 @@
     "builtin/access.h",
     "builtin/address_space.cc",
     "builtin/address_space.h",
+    "builtin/attribute.cc",
+    "builtin/attribute.h",
     "builtin/builtin.cc",
     "builtin/builtin.h",
     "builtin/builtin_value.cc",
@@ -1311,6 +1313,7 @@
     sources = [
       "builtin/access_test.cc",
       "builtin/address_space_test.cc",
+      "builtin/attribute_test.cc",
       "builtin/builtin_test.cc",
       "builtin/builtin_value_test.cc",
       "builtin/diagnostic_rule_test.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index fff86d0..a3835b7 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -555,6 +555,7 @@
 
 tint_generated(builtin/access BENCH TEST)
 tint_generated(builtin/address_space BENCH TEST)
+tint_generated(builtin/attribute BENCH TEST)
 tint_generated(builtin/builtin BENCH TEST)
 tint_generated(builtin/builtin_value BENCH TEST)
 tint_generated(builtin/diagnostic_rule BENCH TEST)
diff --git a/src/tint/builtin/attribute.cc b/src/tint/builtin/attribute.cc
new file mode 100644
index 0000000..d20b77c
--- /dev/null
+++ b/src/tint/builtin/attribute.cc
@@ -0,0 +1,117 @@
+// 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.
+
+////////////////////////////////////////////////////////////////////////////////
+// File generated by tools/src/cmd/gen
+// using the template:
+//   src/tint/builtin/attribute.cc.tmpl
+//
+// Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
+#include "src/tint/builtin/attribute.h"
+
+namespace tint::builtin {
+
+/// ParseAttribute parses a Attribute from a string.
+/// @param str the string to parse
+/// @returns the parsed enum, or Attribute::kUndefined if the string could not be parsed.
+Attribute ParseAttribute(std::string_view str) {
+    if (str == "align") {
+        return Attribute::kAlign;
+    }
+    if (str == "binding") {
+        return Attribute::kBinding;
+    }
+    if (str == "builtin") {
+        return Attribute::kBuiltin;
+    }
+    if (str == "compute") {
+        return Attribute::kCompute;
+    }
+    if (str == "diagnostic") {
+        return Attribute::kDiagnostic;
+    }
+    if (str == "fragment") {
+        return Attribute::kFragment;
+    }
+    if (str == "group") {
+        return Attribute::kGroup;
+    }
+    if (str == "id") {
+        return Attribute::kId;
+    }
+    if (str == "interpolate") {
+        return Attribute::kInterpolate;
+    }
+    if (str == "invariant") {
+        return Attribute::kInvariant;
+    }
+    if (str == "location") {
+        return Attribute::kLocation;
+    }
+    if (str == "must_use") {
+        return Attribute::kMustUse;
+    }
+    if (str == "size") {
+        return Attribute::kSize;
+    }
+    if (str == "vertex") {
+        return Attribute::kVertex;
+    }
+    if (str == "workgroup_size") {
+        return Attribute::kWorkgroupSize;
+    }
+    return Attribute::kUndefined;
+}
+
+std::ostream& operator<<(std::ostream& out, Attribute value) {
+    switch (value) {
+        case Attribute::kUndefined:
+            return out << "undefined";
+        case Attribute::kAlign:
+            return out << "align";
+        case Attribute::kBinding:
+            return out << "binding";
+        case Attribute::kBuiltin:
+            return out << "builtin";
+        case Attribute::kCompute:
+            return out << "compute";
+        case Attribute::kDiagnostic:
+            return out << "diagnostic";
+        case Attribute::kFragment:
+            return out << "fragment";
+        case Attribute::kGroup:
+            return out << "group";
+        case Attribute::kId:
+            return out << "id";
+        case Attribute::kInterpolate:
+            return out << "interpolate";
+        case Attribute::kInvariant:
+            return out << "invariant";
+        case Attribute::kLocation:
+            return out << "location";
+        case Attribute::kMustUse:
+            return out << "must_use";
+        case Attribute::kSize:
+            return out << "size";
+        case Attribute::kVertex:
+            return out << "vertex";
+        case Attribute::kWorkgroupSize:
+            return out << "workgroup_size";
+    }
+    return out << "<unknown>";
+}
+
+}  // namespace tint::builtin
diff --git a/src/tint/builtin/attribute.cc.tmpl b/src/tint/builtin/attribute.cc.tmpl
new file mode 100644
index 0000000..dde123a
--- /dev/null
+++ b/src/tint/builtin/attribute.cc.tmpl
@@ -0,0 +1,25 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/src/cmd/gen to generate attribute.cc
+
+To update the generated file, run:
+    ./tools/run gen
+
+See:
+* tools/src/cmd/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+{{- Import "src/tint/templates/enums.tmpl.inc" -}}
+{{- $enum := (Sem.Enum "attribute") -}}
+
+#include "src/tint/builtin/attribute.h"
+
+namespace tint::builtin {
+
+{{ Eval "ParseEnum" $enum}}
+
+{{ Eval "EnumOStream" $enum}}
+
+}  // namespace tint::builtin
diff --git a/src/tint/builtin/attribute.h b/src/tint/builtin/attribute.h
new file mode 100644
index 0000000..bf95458
--- /dev/null
+++ b/src/tint/builtin/attribute.h
@@ -0,0 +1,72 @@
+// 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.
+
+////////////////////////////////////////////////////////////////////////////////
+// File generated by tools/src/cmd/gen
+// using the template:
+//   src/tint/builtin/attribute.h.tmpl
+//
+// Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef SRC_TINT_BUILTIN_ATTRIBUTE_H_
+#define SRC_TINT_BUILTIN_ATTRIBUTE_H_
+
+#include <ostream>
+
+/// \cond DO_NOT_DOCUMENT
+/// There is a bug in doxygen where this enum conflicts with the ast::Attribute
+/// and generates invalid documentation errors.
+namespace tint::builtin {
+
+/// Address space of a given pointer.
+enum class Attribute {
+    kUndefined,
+    kAlign,
+    kBinding,
+    kBuiltin,
+    kCompute,
+    kDiagnostic,
+    kFragment,
+    kGroup,
+    kId,
+    kInterpolate,
+    kInvariant,
+    kLocation,
+    kMustUse,
+    kSize,
+    kVertex,
+    kWorkgroupSize,
+};
+
+/// @param out the std::ostream to write to
+/// @param value the Attribute
+/// @returns `out` so calls can be chained
+std::ostream& operator<<(std::ostream& out, Attribute value);
+
+/// ParseAttribute parses a Attribute from a string.
+/// @param str the string to parse
+/// @returns the parsed enum, or Attribute::kUndefined if the string could not be parsed.
+Attribute ParseAttribute(std::string_view str);
+
+constexpr const char* kAttributeStrings[] = {
+    "align",    "binding",  "builtin", "compute",     "diagnostic",
+    "fragment", "group",    "id",      "interpolate", "invariant",
+    "location", "must_use", "size",    "vertex",      "workgroup_size",
+};
+
+}  // namespace tint::builtin
+/// \endcond
+
+#endif  // SRC_TINT_BUILTIN_ATTRIBUTE_H_
diff --git a/src/tint/builtin/attribute.h.tmpl b/src/tint/builtin/attribute.h.tmpl
new file mode 100644
index 0000000..32ca6a3
--- /dev/null
+++ b/src/tint/builtin/attribute.h.tmpl
@@ -0,0 +1,33 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/src/cmd/gen to generate attribute.h
+
+To update the generated file, run:
+    ./tools/run gen
+
+See:
+* tools/src/cmd/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+{{- Import "src/tint/templates/enums.tmpl.inc" -}}
+{{- $enum := (Sem.Enum "attribute") -}}
+
+#ifndef SRC_TINT_BUILTIN_ATTRIBUTE_H_
+#define SRC_TINT_BUILTIN_ATTRIBUTE_H_
+
+#include <ostream>
+
+/// \cond DO_NOT_DOCUMENT
+/// There is a bug in doxygen where this enum conflicts with the ast::Attribute
+/// and generates invalid documentation errors.
+namespace tint::builtin {
+
+/// Address space of a given pointer.
+{{ Eval "DeclareEnum" $enum}}
+
+}  // namespace tint::builtin
+/// \endcond
+
+#endif  // SRC_TINT_BUILTIN_ATTRIBUTE_H_
diff --git a/src/tint/builtin/attribute_bench.cc b/src/tint/builtin/attribute_bench.cc
new file mode 100644
index 0000000..5d12ab3
--- /dev/null
+++ b/src/tint/builtin/attribute_bench.cc
@@ -0,0 +1,151 @@
+// 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.
+
+////////////////////////////////////////////////////////////////////////////////
+// File generated by tools/src/cmd/gen
+// using the template:
+//   src/tint/builtin/attribute_bench.cc.tmpl
+//
+// Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
+#include "src/tint/builtin/attribute.h"
+
+#include <array>
+
+#include "benchmark/benchmark.h"
+
+namespace tint::builtin {
+namespace {
+
+void AttributeParser(::benchmark::State& state) {
+    const char* kStrings[] = {
+        "alccn",
+        "3g",
+        "aVign",
+        "align",
+        "alig1",
+        "qqlJn",
+        "alill7n",
+        "ppqqndnHH",
+        "bicv",
+        "bndiGg",
+        "binding",
+        "bindiivg",
+        "8WWnding",
+        "bxxding",
+        "bXltigg",
+        "ultXn",
+        "buil3in",
+        "builtin",
+        "Euiltin",
+        "bPTTltin",
+        "builtdxx",
+        "c44mpute",
+        "coSSpuVVe",
+        "RomR22e",
+        "compute",
+        "cFpu9e",
+        "comute",
+        "VOORRHte",
+        "dyagnstic",
+        "d77agnnnsllrrc",
+        "dia400ostic",
+        "diagnostic",
+        "danstooc",
+        "dignszzic",
+        "d11ansppiic",
+        "XXragment",
+        "fIIa9955nnnt",
+        "aarHHgmenYSS",
+        "fragment",
+        "fkkaet",
+        "gjamRRn",
+        "fabmnt",
+        "gjoup",
+        "goup",
+        "goq",
+        "group",
+        "Nroup",
+        "govv",
+        "gruQQ",
+        "r",
+        "jd",
+        "NNw",
+        "id",
+        "i",
+        "rrd",
+        "iG",
+        "FFnterpolate",
+        "iEtrplat",
+        "inerporrate",
+        "interpolate",
+        "inteplate",
+        "XterJJoDate",
+        "inepol8t",
+        "nvark1n",
+        "invriant",
+        "Jnvarant",
+        "invariant",
+        "invaricnt",
+        "invariaOt",
+        "invttKK_ianvv",
+        "lxxcati8",
+        "Focqq__o",
+        "locaiqqn",
+        "location",
+        "loc33tio6",
+        "ltto6at9QQn",
+        "loc66tio",
+        "mOxt_u6zz",
+        "musyy_use",
+        "mHH_use",
+        "must_use",
+        "qWW4st_se",
+        "musOO_se",
+        "ust_uYe",
+        "i",
+        "Fie",
+        "siw",
+        "size",
+        "zff",
+        "sizqK",
+        "s3zmm",
+        "ertex",
+        "vereq",
+        "vbtbbx",
+        "vertex",
+        "irtex",
+        "vOOteq",
+        "vertTvvx",
+        "woFFkgroup_size",
+        "wfr00grPupsiQe",
+        "workgrouP_size",
+        "workgroup_size",
+        "workgroup77sise",
+        "RRobbkgroupCsize",
+        "wXXrkgroup_size",
+    };
+    for (auto _ : state) {
+        for (auto* str : kStrings) {
+            auto result = ParseAttribute(str);
+            benchmark::DoNotOptimize(result);
+        }
+    }
+}
+
+BENCHMARK(AttributeParser);
+
+}  // namespace
+}  // namespace tint::builtin
diff --git a/src/tint/builtin/attribute_bench.cc.tmpl b/src/tint/builtin/attribute_bench.cc.tmpl
new file mode 100644
index 0000000..8f65de0
--- /dev/null
+++ b/src/tint/builtin/attribute_bench.cc.tmpl
@@ -0,0 +1,29 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/src/cmd/gen to generate attribute_bench.cc
+
+To update the generated file, run:
+    ./tools/run gen
+
+See:
+* tools/src/cmd/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+{{- Import "src/tint/templates/enums.tmpl.inc" -}}
+{{- $enum := (Sem.Enum "attribute") -}}
+
+#include "src/tint/builtin/attribute.h"
+
+#include <array>
+
+#include "benchmark/benchmark.h"
+
+namespace tint::builtin {
+namespace {
+
+{{ Eval "BenchmarkParseEnum" $enum }}
+
+}  // namespace
+}  // namespace tint::builtin
diff --git a/src/tint/builtin/attribute_test.cc b/src/tint/builtin/attribute_test.cc
new file mode 100644
index 0000000..9919467
--- /dev/null
+++ b/src/tint/builtin/attribute_test.cc
@@ -0,0 +1,135 @@
+// 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.
+
+////////////////////////////////////////////////////////////////////////////////
+// File generated by tools/src/cmd/gen
+// using the template:
+//   src/tint/builtin/attribute_test.cc.tmpl
+//
+// Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
+#include "src/tint/builtin/attribute.h"
+
+#include <gtest/gtest.h>
+
+#include <string>
+
+#include "src/tint/utils/string.h"
+
+namespace tint::builtin {
+namespace {
+
+namespace parse_print_tests {
+
+struct Case {
+    const char* string;
+    Attribute value;
+};
+
+inline std::ostream& operator<<(std::ostream& out, Case c) {
+    return out << "'" << std::string(c.string) << "'";
+}
+
+static constexpr Case kValidCases[] = {
+    {"align", Attribute::kAlign},
+    {"binding", Attribute::kBinding},
+    {"builtin", Attribute::kBuiltin},
+    {"compute", Attribute::kCompute},
+    {"diagnostic", Attribute::kDiagnostic},
+    {"fragment", Attribute::kFragment},
+    {"group", Attribute::kGroup},
+    {"id", Attribute::kId},
+    {"interpolate", Attribute::kInterpolate},
+    {"invariant", Attribute::kInvariant},
+    {"location", Attribute::kLocation},
+    {"must_use", Attribute::kMustUse},
+    {"size", Attribute::kSize},
+    {"vertex", Attribute::kVertex},
+    {"workgroup_size", Attribute::kWorkgroupSize},
+};
+
+static constexpr Case kInvalidCases[] = {
+    {"alccn", Attribute::kUndefined},
+    {"3g", Attribute::kUndefined},
+    {"aVign", Attribute::kUndefined},
+    {"bind1ng", Attribute::kUndefined},
+    {"bqnJing", Attribute::kUndefined},
+    {"bindin7ll", Attribute::kUndefined},
+    {"ppqqiliHH", Attribute::kUndefined},
+    {"bucv", Attribute::kUndefined},
+    {"biltGn", Attribute::kUndefined},
+    {"compiive", Attribute::kUndefined},
+    {"8WWmpute", Attribute::kUndefined},
+    {"cxxpute", Attribute::kUndefined},
+    {"dXagnosigg", Attribute::kUndefined},
+    {"dagnXuVc", Attribute::kUndefined},
+    {"diagnosti3", Attribute::kUndefined},
+    {"fraEment", Attribute::kUndefined},
+    {"PPagTTent", Attribute::kUndefined},
+    {"xxragddnt", Attribute::kUndefined},
+    {"g44oup", Attribute::kUndefined},
+    {"grVVSSp", Attribute::kUndefined},
+    {"22RRp", Attribute::kUndefined},
+    {"d", Attribute::kUndefined},
+    {"i", Attribute::kUndefined},
+    {"OVd", Attribute::kUndefined},
+    {"inyerpolae", Attribute::kUndefined},
+    {"rrnterpolll77Ge", Attribute::kUndefined},
+    {"inte4pol00te", Attribute::kUndefined},
+    {"inoornt", Attribute::kUndefined},
+    {"inzzriat", Attribute::kUndefined},
+    {"n11pariiin", Attribute::kUndefined},
+    {"XXocation", Attribute::kUndefined},
+    {"lIIc9955nnon", Attribute::kUndefined},
+    {"aaoHHatioYSS", Attribute::kUndefined},
+    {"mkksue", Attribute::kUndefined},
+    {"gjs_RRs", Attribute::kUndefined},
+    {"msb_se", Attribute::kUndefined},
+    {"jize", Attribute::kUndefined},
+    {"sze", Attribute::kUndefined},
+    {"qz", Attribute::kUndefined},
+    {"vNNtex", Attribute::kUndefined},
+    {"vevvx", Attribute::kUndefined},
+    {"veQQex", Attribute::kUndefined},
+    {"workgrrupffie", Attribute::kUndefined},
+    {"workgroup_sije", Attribute::kUndefined},
+    {"workgoupNNwsiz8", Attribute::kUndefined},
+};
+
+using AttributeParseTest = testing::TestWithParam<Case>;
+
+TEST_P(AttributeParseTest, Parse) {
+    const char* string = GetParam().string;
+    Attribute expect = GetParam().value;
+    EXPECT_EQ(expect, ParseAttribute(string));
+}
+
+INSTANTIATE_TEST_SUITE_P(ValidCases, AttributeParseTest, testing::ValuesIn(kValidCases));
+INSTANTIATE_TEST_SUITE_P(InvalidCases, AttributeParseTest, testing::ValuesIn(kInvalidCases));
+
+using AttributePrintTest = testing::TestWithParam<Case>;
+
+TEST_P(AttributePrintTest, Print) {
+    Attribute value = GetParam().value;
+    const char* expect = GetParam().string;
+    EXPECT_EQ(expect, utils::ToString(value));
+}
+
+INSTANTIATE_TEST_SUITE_P(ValidCases, AttributePrintTest, testing::ValuesIn(kValidCases));
+
+}  // namespace parse_print_tests
+
+}  // namespace
+}  // namespace tint::builtin
diff --git a/src/tint/builtin/attribute_test.cc.tmpl b/src/tint/builtin/attribute_test.cc.tmpl
new file mode 100644
index 0000000..27dd8a9
--- /dev/null
+++ b/src/tint/builtin/attribute_test.cc.tmpl
@@ -0,0 +1,31 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/src/cmd/gen to generate attribute_test.cc
+
+To update the generated file, run:
+    ./tools/run gen
+
+See:
+* tools/src/cmd/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+{{- Import "src/tint/templates/enums.tmpl.inc" -}}
+{{- $enum := (Sem.Enum "attribute") -}}
+
+#include "src/tint/builtin/attribute.h"
+
+#include <gtest/gtest.h>
+
+#include <string>
+
+#include "src/tint/utils/string.h"
+
+namespace tint::builtin {
+namespace {
+
+{{ Eval "TestParsePrintEnum" $enum}}
+
+}  // namespace
+}  // namespace tint::builtin
diff --git a/src/tint/intrinsics.def b/src/tint/intrinsics.def
index 45b4b04..a89eab5 100644
--- a/src/tint/intrinsics.def
+++ b/src/tint/intrinsics.def
@@ -211,6 +211,29 @@
   texture_external
 }
 
+// https://gpuweb.github.io/gpuweb/wgsl/#attributes
+// Notes:
+//  * `diagnostic` is listed, even though it is a keyword, so it appears in suggested alternatives
+//  * `const` is not listed here as it can not be written in user programs and should not show up
+//     in error messages
+enum attribute {
+  align
+  binding
+  builtin
+  compute
+  diagnostic
+  fragment
+  group
+  id
+  interpolate
+  invariant
+  location
+  must_use
+  size
+  vertex
+  workgroup_size
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // WGSL primitive types                                                       //
 // Types may be decorated with @precedence(N) to prioritize which type        //
diff --git a/src/tint/reader/wgsl/parser_impl.cc b/src/tint/reader/wgsl/parser_impl.cc
index 6b4b8d7..13b4741 100644
--- a/src/tint/reader/wgsl/parser_impl.cc
+++ b/src/tint/reader/wgsl/parser_impl.cc
@@ -34,6 +34,7 @@
 #include "src/tint/ast/unary_op_expression.h"
 #include "src/tint/ast/variable_decl_statement.h"
 #include "src/tint/ast/workgroup_attribute.h"
+#include "src/tint/builtin/attribute.h"
 #include "src/tint/reader/wgsl/classify_template_args.h"
 #include "src/tint/reader/wgsl/lexer.h"
 #include "src/tint/type/depth_texture.h"
@@ -2988,9 +2989,12 @@
 Maybe<const ast::Attribute*> ParserImpl::attribute() {
     // Note, the ATTR is matched by the called `attribute_list` in this case, so it is not matched
     // here and this has to be an attribute.
-    auto& t = next();
+    auto& t = peek();
 
-    if (t.Is(Token::Type::kDiagnostic)) {
+    if (match(Token::Type::kConst)) {
+        return add_error(t.source(), "const attribute may not appear in shaders");
+    }
+    if (match(Token::Type::kDiagnostic)) {
         auto control = expect_diagnostic_control();
         if (control.errored) {
             return Failure::kErrored;
@@ -2998,21 +3002,41 @@
         return create<ast::DiagnosticAttribute>(t.source(), std::move(control.value));
     }
 
-    if (!t.IsIdentifier()) {
-        return Failure::kNoMatch;
+    auto attr = expect_enum("attribute", builtin::ParseAttribute, builtin::kAttributeStrings);
+    if (attr.errored) {
+        return Failure::kErrored;
     }
 
-    utils::Vector<const ast::Expression*, 2> exprs;
-    auto parse = [&](uint32_t min, uint32_t max) -> Maybe<bool> {
-        // Handle no parameter items which should have no parens
-        if (min == 0) {
-            auto& p = peek();
-            if (p.Is(Token::Type::kParenLeft)) {
-                return add_error(p.source(), t.to_str() + " attribute doesn't take parenthesis");
-            }
-            return true;
-        }
+    uint32_t min = 1;
+    uint32_t max = 1;
+    switch (attr.value) {
+        case builtin::Attribute::kCompute:
+        case builtin::Attribute::kFragment:
+        case builtin::Attribute::kInvariant:
+        case builtin::Attribute::kMustUse:
+        case builtin::Attribute::kVertex:
+            min = 0;
+            max = 0;
+            break;
+        case builtin::Attribute::kInterpolate:
+            max = 2;
+            break;
+        case builtin::Attribute::kWorkgroupSize:
+            max = 3;
+            break;
+        default:
+            break;
+    }
 
+    utils::Vector<const ast::Expression*, 2> args;
+
+    // Handle no parameter items which should have no parens
+    if (min == 0) {
+        auto& t2 = peek();
+        if (match(Token::Type::kParenLeft)) {
+            return add_error(t2.source(), t.to_str() + " attribute doesn't take parenthesis");
+        }
+    } else {
         auto res = expect_paren_block(t.to_str() + " attribute", [&]() -> Expect<bool> {
             while (continue_parsing()) {
                 if (peek().Is(Token::Type::kParenRight)) {
@@ -3023,7 +3047,7 @@
                 if (expr.errored) {
                     return Failure::kErrored;
                 }
-                exprs.Push(expr.value);
+                args.Push(expr.value);
 
                 if (!match(Token::Type::kComma)) {
                     break;
@@ -3035,138 +3059,54 @@
             return Failure::kErrored;
         }
 
-        if (exprs.IsEmpty() || exprs.Length() < min) {
+        if (args.IsEmpty() || args.Length() < min) {
             return add_error(t.source(),
                              t.to_str() + " expects" + (min != max ? " at least " : " ") +
-                                 std::to_string(min) + " argument" + (min > 1 ? "s" : ""));
+                                 std::to_string(min) + " argument" + (min != 1 ? "s" : ""));
         }
-
-        if (exprs.Length() > max) {
+        if (args.Length() > max) {
             return add_error(t.source(),
                              t.to_str() + " expects" + (min != max ? " at most " : " ") +
-                                 std::to_string(max) + " argument" + (max > 1 ? "s" : "") +
-                                 ", got " + std::to_string(exprs.Length()));
+                                 std::to_string(max) + " argument" + (max != 1 ? "s" : "") +
+                                 ", got " + std::to_string(args.Length()));
         }
-        return true;
-    };
-
-    if (t == "align") {
-        auto res = parse(1, 1);
-        if (res.errored) {
-            return Failure::kErrored;
-        }
-        return create<ast::StructMemberAlignAttribute>(t.source(), exprs[0]);
     }
 
-    if (t == "binding") {
-        auto res = parse(1, 1);
-        if (res.errored) {
-            return Failure::kErrored;
-        }
-        return create<ast::BindingAttribute>(t.source(), exprs[0]);
+    switch (attr.value) {
+        case builtin::Attribute::kAlign:
+            return create<ast::StructMemberAlignAttribute>(t.source(), args[0]);
+        case builtin::Attribute::kBinding:
+            return create<ast::BindingAttribute>(t.source(), args[0]);
+        case builtin::Attribute::kBuiltin:
+            return create<ast::BuiltinAttribute>(t.source(), args[0]);
+        case builtin::Attribute::kCompute:
+            return create<ast::StageAttribute>(t.source(), ast::PipelineStage::kCompute);
+        case builtin::Attribute::kFragment:
+            return create<ast::StageAttribute>(t.source(), ast::PipelineStage::kFragment);
+        case builtin::Attribute::kGroup:
+            return create<ast::GroupAttribute>(t.source(), args[0]);
+        case builtin::Attribute::kId:
+            return create<ast::IdAttribute>(t.source(), args[0]);
+        case builtin::Attribute::kInterpolate:
+            return create<ast::InterpolateAttribute>(t.source(), args[0],
+                                                     args.Length() == 2 ? args[1] : nullptr);
+        case builtin::Attribute::kInvariant:
+            return create<ast::InvariantAttribute>(t.source());
+        case builtin::Attribute::kLocation:
+            return builder_.Location(t.source(), args[0]);
+        case builtin::Attribute::kMustUse:
+            return create<ast::MustUseAttribute>(t.source());
+        case builtin::Attribute::kSize:
+            return builder_.MemberSize(t.source(), args[0]);
+        case builtin::Attribute::kVertex:
+            return create<ast::StageAttribute>(t.source(), ast::PipelineStage::kVertex);
+        case builtin::Attribute::kWorkgroupSize:
+            return create<ast::WorkgroupAttribute>(t.source(), args[0],
+                                                   args.Length() > 1 ? args[1] : nullptr,
+                                                   args.Length() > 2 ? args[2] : nullptr);
+        default:
+            return Failure::kNoMatch;
     }
-
-    if (t == "builtin") {
-        auto res = parse(1, 1);
-        if (res.errored) {
-            return Failure::kErrored;
-        }
-        return create<ast::BuiltinAttribute>(t.source(), exprs[0]);
-    }
-
-    if (t == "compute") {
-        auto res = parse(0, 0);
-        if (res.errored) {
-            return Failure::kErrored;
-        }
-        return create<ast::StageAttribute>(t.source(), ast::PipelineStage::kCompute);
-    }
-
-    // Note, `const` is not valid in a WGSL source file, it's internal only
-
-    if (t == "fragment") {
-        auto res = parse(0, 0);
-        if (res.errored) {
-            return Failure::kErrored;
-        }
-        return create<ast::StageAttribute>(t.source(), ast::PipelineStage::kFragment);
-    }
-
-    if (t == "group") {
-        auto res = parse(1, 1);
-        if (res.errored) {
-            return Failure::kErrored;
-        }
-        return create<ast::GroupAttribute>(t.source(), exprs[0]);
-    }
-
-    if (t == "id") {
-        auto res = parse(1, 1);
-        if (res.errored) {
-            return Failure::kErrored;
-        }
-        return create<ast::IdAttribute>(t.source(), exprs[0]);
-    }
-
-    if (t == "interpolate") {
-        auto res = parse(1, 2);
-        if (res.errored) {
-            return Failure::kErrored;
-        }
-        return create<ast::InterpolateAttribute>(t.source(), exprs[0],
-                                                 exprs.Length() == 2 ? exprs[1] : nullptr);
-    }
-
-    if (t == "invariant") {
-        auto res = parse(0, 0);
-        if (res.errored) {
-            return Failure::kErrored;
-        }
-        return create<ast::InvariantAttribute>(t.source());
-    }
-
-    if (t == "location") {
-        auto res = parse(1, 1);
-        if (res.errored) {
-            return Failure::kErrored;
-        }
-        return builder_.Location(t.source(), exprs[0]);
-    }
-
-    if (t == "must_use") {
-        auto res = parse(0, 0);
-        if (res.errored) {
-            return Failure::kErrored;
-        }
-        return create<ast::MustUseAttribute>(t.source());
-    }
-
-    if (t == "size") {
-        auto res = parse(1, 1);
-        if (res.errored) {
-            return Failure::kErrored;
-        }
-        return builder_.MemberSize(t.source(), exprs[0]);
-    }
-
-    if (t == "vertex") {
-        auto res = parse(0, 0);
-        if (res.errored) {
-            return Failure::kErrored;
-        }
-        return create<ast::StageAttribute>(t.source(), ast::PipelineStage::kVertex);
-    }
-
-    if (t == "workgroup_size") {
-        auto res = parse(1, 3);
-        if (res.errored) {
-            return Failure::kErrored;
-        }
-        return create<ast::WorkgroupAttribute>(t.source(), exprs[0],
-                                               exprs.Length() > 1 ? exprs[1] : nullptr,
-                                               exprs.Length() > 2 ? exprs[2] : nullptr);
-    }
-    return Failure::kNoMatch;
 }
 
 bool ParserImpl::expect_attributes_consumed(utils::VectorRef<const ast::Attribute*> in) {
diff --git a/src/tint/reader/wgsl/parser_impl_error_msg_test.cc b/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
index ccee818..85e55cb 100644
--- a/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
@@ -59,6 +59,14 @@
 )");
 }
 
+TEST_F(ParserImplErrorTest, ConstAttributeInvalid) {
+    EXPECT("@const fn main() { }",
+           R"(test.wgsl:1:2 error: const attribute may not appear in shaders
+@const fn main() { }
+ ^^^^^
+)");
+}
+
 TEST_F(ParserImplErrorTest, IndexExprInvalidExpr) {
     EXPECT("fn f() { x = y[^]; }",
            R"(test.wgsl:1:16 error: unable to parse expression inside []
diff --git a/src/tint/reader/wgsl/parser_impl_error_resync_test.cc b/src/tint/reader/wgsl/parser_impl_error_resync_test.cc
index 9e37b51..f3e306a 100644
--- a/src/tint/reader/wgsl/parser_impl_error_resync_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_error_resync_test.cc
@@ -54,12 +54,9 @@
      ^
 
 test.wgsl:4:2 error: expected attribute
+Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
 @_ fn -> {}
  ^
-
-test.wgsl:4:7 error: expected identifier for function declaration
-@_ fn -> {}
-      ^^
 )");
 }
 
@@ -125,6 +122,7 @@
          ^^^^
 
 test.wgsl:7:6 error: expected attribute
+Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
     @- x : i32,
      ^
 )");
diff --git a/src/tint/reader/wgsl/parser_impl_function_attribute_list_test.cc b/src/tint/reader/wgsl/parser_impl_function_attribute_list_test.cc
index 785d23c..ae43a0f 100644
--- a/src/tint/reader/wgsl/parser_impl_function_attribute_list_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_function_attribute_list_test.cc
@@ -51,7 +51,9 @@
     EXPECT_TRUE(attrs.errored);
     EXPECT_FALSE(attrs.matched);
     EXPECT_TRUE(attrs.value.IsEmpty());
-    EXPECT_EQ(p->error(), "1:2: expected attribute");
+    EXPECT_EQ(p->error(), R"(1:2: expected attribute
+Did you mean 'invariant'?
+Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
 }
 
 }  // namespace
diff --git a/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc b/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc
index 24ad020..07775c5 100644
--- a/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc
@@ -50,7 +50,9 @@
     EXPECT_TRUE(attrs.errored);
     EXPECT_FALSE(attrs.matched);
     EXPECT_TRUE(attrs.value.IsEmpty());
-    EXPECT_EQ(p->error(), R"(1:2: expected attribute)");
+    EXPECT_EQ(p->error(), R"(1:2: expected attribute
+Did you mean 'invariant'?
+Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
 }
 
 }  // namespace