tint/reader: Enable 'const' parsing for unit-tests

This enables the WGSL parsing of 'const' for only tint_unittests,
allowing code to be split up into smaller chunks for review.

Once all the logic is submitted to handle 'const', the test-only
flag will be removed, and 'const' will be enabled for production.

Bug: tint:1580
Change-Id: I3189b6bd15445ecc3fa1cd6f568885e7ba3c91c0
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/94680
Reviewed-by: David Neto <dneto@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/ast/const.cc b/src/tint/ast/const.cc
index 6ae8e8c..e13cc25 100644
--- a/src/tint/ast/const.cc
+++ b/src/tint/ast/const.cc
@@ -34,6 +34,10 @@
 
 Const::~Const() = default;
 
+const char* Const::Kind() const {
+    return "const";
+}
+
 const Const* Const::Clone(CloneContext* ctx) const {
     auto src = ctx->Clone(source);
     auto sym = ctx->Clone(symbol);
diff --git a/src/tint/ast/const.h b/src/tint/ast/const.h
index 39ac6c3..32e3ddd 100644
--- a/src/tint/ast/const.h
+++ b/src/tint/ast/const.h
@@ -52,6 +52,9 @@
     /// Destructor
     ~Const() override;
 
+    /// @returns "const"
+    const char* Kind() const override;
+
     /// Clones this node and all transitive child nodes using the `CloneContext`
     /// `ctx`.
     /// @param ctx the clone context
diff --git a/src/tint/ast/let.cc b/src/tint/ast/let.cc
index e771c4d..8db8ec1 100644
--- a/src/tint/ast/let.cc
+++ b/src/tint/ast/let.cc
@@ -34,6 +34,10 @@
 
 Let::~Let() = default;
 
+const char* Let::Kind() const {
+    return "let";
+}
+
 const Let* Let::Clone(CloneContext* ctx) const {
     auto src = ctx->Clone(source);
     auto sym = ctx->Clone(symbol);
diff --git a/src/tint/ast/let.h b/src/tint/ast/let.h
index 2b0a6aa..2c1ad7c 100644
--- a/src/tint/ast/let.h
+++ b/src/tint/ast/let.h
@@ -49,6 +49,9 @@
     /// Destructor
     ~Let() override;
 
+    /// @returns "let"
+    const char* Kind() const override;
+
     /// Clones this node and all transitive child nodes using the `CloneContext`
     /// `ctx`.
     /// @param ctx the clone context
diff --git a/src/tint/ast/override.cc b/src/tint/ast/override.cc
index f494bc8..efd048d 100644
--- a/src/tint/ast/override.cc
+++ b/src/tint/ast/override.cc
@@ -32,6 +32,10 @@
 
 Override::~Override() = default;
 
+const char* Override::Kind() const {
+    return "override";
+}
+
 const Override* Override::Clone(CloneContext* ctx) const {
     auto src = ctx->Clone(source);
     auto sym = ctx->Clone(symbol);
diff --git a/src/tint/ast/override.h b/src/tint/ast/override.h
index 168b9b2..98319e5 100644
--- a/src/tint/ast/override.h
+++ b/src/tint/ast/override.h
@@ -50,6 +50,9 @@
     /// Destructor
     ~Override() override;
 
+    /// @returns "override"
+    const char* Kind() const override;
+
     /// Clones this node and all transitive child nodes using the `CloneContext`
     /// `ctx`.
     /// @param ctx the clone context
diff --git a/src/tint/ast/parameter.cc b/src/tint/ast/parameter.cc
index b7ea3b1..ea3c51f 100644
--- a/src/tint/ast/parameter.cc
+++ b/src/tint/ast/parameter.cc
@@ -31,6 +31,10 @@
 
 Parameter::~Parameter() = default;
 
+const char* Parameter::Kind() const {
+    return "parameter";
+}
+
 const Parameter* Parameter::Clone(CloneContext* ctx) const {
     auto src = ctx->Clone(source);
     auto sym = ctx->Clone(symbol);
diff --git a/src/tint/ast/parameter.h b/src/tint/ast/parameter.h
index d3ecf8d..eb4b688 100644
--- a/src/tint/ast/parameter.h
+++ b/src/tint/ast/parameter.h
@@ -51,6 +51,9 @@
     /// Destructor
     ~Parameter() override;
 
+    /// @returns "parameter"
+    const char* Kind() const override;
+
     /// Clones this node and all transitive child nodes using the `CloneContext`
     /// `ctx`.
     /// @param ctx the clone context
diff --git a/src/tint/ast/var.cc b/src/tint/ast/var.cc
index 622aa03..854503e 100644
--- a/src/tint/ast/var.cc
+++ b/src/tint/ast/var.cc
@@ -36,6 +36,10 @@
 
 Var::~Var() = default;
 
+const char* Var::Kind() const {
+    return "var";
+}
+
 const Var* Var::Clone(CloneContext* ctx) const {
     auto src = ctx->Clone(source);
     auto sym = ctx->Clone(symbol);
diff --git a/src/tint/ast/var.h b/src/tint/ast/var.h
index fd03580..565ebbb 100644
--- a/src/tint/ast/var.h
+++ b/src/tint/ast/var.h
@@ -65,6 +65,9 @@
     /// Destructor
     ~Var() override;
 
+    /// @returns "var"
+    const char* Kind() const override;
+
     /// Clones this node and all transitive child nodes using the `CloneContext`
     /// `ctx`.
     /// @param ctx the clone context
diff --git a/src/tint/ast/variable.h b/src/tint/ast/variable.h
index bc25753..631fbe5 100644
--- a/src/tint/ast/variable.h
+++ b/src/tint/ast/variable.h
@@ -77,6 +77,10 @@
     /// @note binding points should only be applied to Var and Parameter types.
     VariableBindingPoint BindingPoint() const;
 
+    /// @returns the kind of the variable, which can be used in diagnostics
+    ///          e.g. "var", "let", "const", etc
+    virtual const char* Kind() const = 0;
+
     /// The variable symbol
     const Symbol symbol;
 
diff --git a/src/tint/reader/wgsl/lexer_test.cc b/src/tint/reader/wgsl/lexer_test.cc
index 712474f..bf32a52 100644
--- a/src/tint/reader/wgsl/lexer_test.cc
+++ b/src/tint/reader/wgsl/lexer_test.cc
@@ -144,18 +144,18 @@
     // Test that line comments are ended by blankspace characters other than
     // space, horizontal tab, left-to-right mark, and right-to-left mark.
     auto* c = GetParam();
-    std::string src = "let// This is a comment";
+    std::string src = "const// This is a comment";
     src += c;
     src += "ident";
     Source::File file("", src);
     Lexer l(&file);
 
     auto t = l.next();
-    EXPECT_TRUE(t.Is(Token::Type::kLet));
+    EXPECT_TRUE(t.Is(Token::Type::kConst));
     EXPECT_EQ(t.source().range.begin.line, 1u);
     EXPECT_EQ(t.source().range.begin.column, 1u);
     EXPECT_EQ(t.source().range.end.line, 1u);
-    EXPECT_EQ(t.source().range.end.column, 4u);
+    EXPECT_EQ(t.source().range.end.column, 6u);
 
     auto is_same_line = [](std::string_view v) {
         return v == kSpace || v == kHTab || v == kL2R || v == kR2L;
diff --git a/src/tint/reader/wgsl/parser_impl.cc b/src/tint/reader/wgsl/parser_impl.cc
index 3f1fad4..f6ab166 100644
--- a/src/tint/reader/wgsl/parser_impl.cc
+++ b/src/tint/reader/wgsl/parser_impl.cc
@@ -47,6 +47,9 @@
 namespace tint::reader::wgsl {
 namespace {
 
+// TODO(crbug.com/tint/1580): Remove when 'const' is fully implemented.
+bool const_enabled = false;
+
 template <typename T>
 using Expect = ParserImpl::Expect<T>;
 
@@ -245,6 +248,11 @@
 
 ParserImpl::~ParserImpl() = default;
 
+// TODO(crbug.com/tint/1580): Remove when 'const' is fully implemented.
+void ParserImpl::EnableConst() {
+    const_enabled = true;
+}
+
 ParserImpl::Failure::Errored ParserImpl::add_error(const Source& source,
                                                    std::string_view err,
                                                    std::string_view use) {
@@ -437,8 +445,12 @@
         }
 
         if (gc.matched) {
-            if (!expect("'let' declaration", Token::Type::kSemicolon)) {
-                return Failure::kErrored;
+            // Avoid the cost of the string allocation for the common no-error case
+            if (!peek().Is(Token::Type::kSemicolon)) {
+                std::string kind = gc->Kind();
+                if (!expect("'" + kind + "' declaration", Token::Type::kSemicolon)) {
+                    return Failure::kErrored;
+                }
             }
 
             builder_.AST().AddGlobalVariable(gc.value);
@@ -561,10 +573,14 @@
 // global_const_initializer
 //  : EQUAL const_expr
 Maybe<const ast::Variable*> ParserImpl::global_constant_decl(ast::AttributeList& attrs) {
+    bool is_const = false;
     bool is_overridable = false;
     const char* use = nullptr;
     if (match(Token::Type::kLet)) {
         use = "'let' declaration";
+    } else if (const_enabled && match(Token::Type::kConst)) {
+        use = "'const' declaration";
+        is_const = true;
     } else if (match(Token::Type::kOverride)) {
         use = "'override' declaration";
         is_overridable = true;
@@ -596,6 +612,13 @@
         initializer = std::move(init.value);
     }
 
+    if (is_const) {
+        return create<ast::Const>(decl->source,                             // source
+                                  builder_.Symbols().Register(decl->name),  // symbol
+                                  decl->type,                               // type
+                                  initializer,                              // constructor
+                                  std::move(attrs));                        // attributes
+    }
     if (is_overridable) {
         return create<ast::Override>(decl->source,                             // source
                                      builder_.Symbols().Register(decl->name),  // symbol
@@ -1769,6 +1792,34 @@
 //   | variable_decl EQUAL logical_or_expression
 //   | CONST variable_ident_decl EQUAL logical_or_expression
 Maybe<const ast::VariableDeclStatement*> ParserImpl::variable_stmt() {
+    if (const_enabled && match(Token::Type::kConst)) {
+        auto decl = expect_variable_ident_decl("'const' declaration",
+                                               /*allow_inferred = */ true);
+        if (decl.errored) {
+            return Failure::kErrored;
+        }
+
+        if (!expect("'const' declaration", Token::Type::kEqual)) {
+            return Failure::kErrored;
+        }
+
+        auto constructor = logical_or_expression();
+        if (constructor.errored) {
+            return Failure::kErrored;
+        }
+        if (!constructor.matched) {
+            return add_error(peek(), "missing constructor for 'const' declaration");
+        }
+
+        auto* const_ = create<ast::Const>(decl->source,                             // source
+                                          builder_.Symbols().Register(decl->name),  // symbol
+                                          decl->type,                               // type
+                                          constructor.value,                        // constructor
+                                          ast::AttributeList{});                    // attributes
+
+        return create<ast::VariableDeclStatement>(decl->source, const_);
+    }
+
     if (match(Token::Type::kLet)) {
         auto decl = expect_variable_ident_decl("'let' declaration",
                                                /*allow_inferred = */ true);
diff --git a/src/tint/reader/wgsl/parser_impl.h b/src/tint/reader/wgsl/parser_impl.h
index c6e696b..89d5163 100644
--- a/src/tint/reader/wgsl/parser_impl.h
+++ b/src/tint/reader/wgsl/parser_impl.h
@@ -72,6 +72,11 @@
     };
 
   public:
+    /// A temporary bodge to enable unit-testing of 'const' variable types while still under active
+    /// development.
+    // TODO(crbug.com/tint/1580): Remove when 'const' is fully implemented.
+    static void EnableConst();
+
     /// Expect is the return type of the parser methods that are expected to
     /// return a parsed value of type T, unless there was an parse error.
     /// In the case of a parse error the called method will have called
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 fc141c0..5e99d34 100644
--- a/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
@@ -471,6 +471,107 @@
 }
 
 TEST_F(ParserImplErrorTest, GlobalDeclConstInvalidIdentifier) {
+    EXPECT("const ^ : i32 = 1;",
+           R"(test.wgsl:1:7 error: expected identifier for 'const' declaration
+const ^ : i32 = 1;
+      ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclConstMissingSemicolon) {
+    EXPECT("const i : i32 = 1",
+           R"(test.wgsl:1:18 error: expected ';' for 'const' declaration
+const i : i32 = 1
+                 ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclConstMissingLParen) {
+    EXPECT("const i : vec2<i32> = vec2<i32>;",
+           R"(test.wgsl:1:32 error: expected '(' for type constructor
+const i : vec2<i32> = vec2<i32>;
+                               ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclConstMissingRParen) {
+    EXPECT("const i : vec2<i32> = vec2<i32>(1., 2.;",
+           R"(test.wgsl:1:39 error: expected ')' for type constructor
+const i : vec2<i32> = vec2<i32>(1., 2.;
+                                      ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclConstBadConstLiteral) {
+    EXPECT("const i : vec2<i32> = vec2<i32>(!);",
+           R"(test.wgsl:1:33 error: unable to parse const_expr
+const i : vec2<i32> = vec2<i32>(!);
+                                ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclConstBadConstLiteralSpaceLessThan) {
+    EXPECT("const i = 1 < 2;",
+           R"(test.wgsl:1:13 error: expected ';' for 'const' declaration
+const i = 1 < 2;
+            ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclConstNotConstExpr) {
+    EXPECT(
+        "const a = 1;\n"
+        "const b = a;",
+        R"(test.wgsl:2:11 error: unable to parse const_expr
+const b = a;
+          ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclConstExprMaxDepth) {
+    uint32_t kMaxDepth = 128;
+
+    std::stringstream src;
+    std::stringstream mkr;
+    src << "const i : i32 = ";
+    mkr << "                ";
+    for (size_t i = 0; i < kMaxDepth + 8; i++) {
+        src << "f32(";
+        if (i < kMaxDepth) {
+            mkr << "    ";
+        } else if (i == kMaxDepth) {
+            mkr << "^^^";
+        }
+    }
+    src << "1.0";
+    for (size_t i = 0; i < 200; i++) {
+        src << ")";
+    }
+    src << ";";
+    std::stringstream err;
+    err << "test.wgsl:1:529 error: maximum parser recursive depth reached\n"
+        << src.str() << "\n"
+        << mkr.str() << "\n";
+    EXPECT(src.str().c_str(), err.str().c_str());
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclConstExprMissingLParen) {
+    EXPECT("const i : vec2<i32> = vec2<i32> 1, 2);",
+           R"(test.wgsl:1:33 error: expected '(' for type constructor
+const i : vec2<i32> = vec2<i32> 1, 2);
+                                ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclConstExprMissingRParen) {
+    EXPECT("const i : vec2<i32> = vec2<i32>(1, 2;",
+           R"(test.wgsl:1:37 error: expected ')' for type constructor
+const i : vec2<i32> = vec2<i32>(1, 2;
+                                    ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclLetInvalidIdentifier) {
     EXPECT("let ^ : i32 = 1;",
            R"(test.wgsl:1:5 error: expected identifier for 'let' declaration
 let ^ : i32 = 1;
@@ -478,7 +579,7 @@
 )");
 }
 
-TEST_F(ParserImplErrorTest, GlobalDeclConstMissingSemicolon) {
+TEST_F(ParserImplErrorTest, GlobalDeclLetMissingSemicolon) {
     EXPECT("let i : i32 = 1",
            R"(test.wgsl:1:16 error: expected ';' for 'let' declaration
 let i : i32 = 1
@@ -486,7 +587,7 @@
 )");
 }
 
-TEST_F(ParserImplErrorTest, GlobalDeclConstMissingLParen) {
+TEST_F(ParserImplErrorTest, GlobalDeclLetMissingLParen) {
     EXPECT("let i : vec2<i32> = vec2<i32>;",
            R"(test.wgsl:1:30 error: expected '(' for type constructor
 let i : vec2<i32> = vec2<i32>;
@@ -494,7 +595,7 @@
 )");
 }
 
-TEST_F(ParserImplErrorTest, GlobalDeclConstMissingRParen) {
+TEST_F(ParserImplErrorTest, GlobalDeclLetMissingRParen) {
     EXPECT("let i : vec2<i32> = vec2<i32>(1., 2.;",
            R"(test.wgsl:1:37 error: expected ')' for type constructor
 let i : vec2<i32> = vec2<i32>(1., 2.;
@@ -502,7 +603,7 @@
 )");
 }
 
-TEST_F(ParserImplErrorTest, GlobalDeclConstBadConstLiteral) {
+TEST_F(ParserImplErrorTest, GlobalDeclLetBadConstLiteral) {
     EXPECT("let i : vec2<i32> = vec2<i32>(!);",
            R"(test.wgsl:1:31 error: unable to parse const_expr
 let i : vec2<i32> = vec2<i32>(!);
@@ -510,7 +611,7 @@
 )");
 }
 
-TEST_F(ParserImplErrorTest, GlobalDeclConstBadConstLiteralSpaceLessThan) {
+TEST_F(ParserImplErrorTest, GlobalDeclLetBadConstLiteralSpaceLessThan) {
     EXPECT("let i = 1 < 2;",
            R"(test.wgsl:1:11 error: expected ';' for 'let' declaration
 let i = 1 < 2;
@@ -518,7 +619,7 @@
 )");
 }
 
-TEST_F(ParserImplErrorTest, GlobalDeclConstNotConstExpr) {
+TEST_F(ParserImplErrorTest, GlobalDeclLetNotConstExpr) {
     EXPECT(
         "let a = 1;\n"
         "let b = a;",
@@ -528,7 +629,7 @@
 )");
 }
 
-TEST_F(ParserImplErrorTest, GlobalDeclConstExprMaxDepth) {
+TEST_F(ParserImplErrorTest, GlobalDeclLetExprMaxDepth) {
     uint32_t kMaxDepth = 128;
 
     std::stringstream src;
@@ -555,7 +656,7 @@
     EXPECT(src.str().c_str(), err.str().c_str());
 }
 
-TEST_F(ParserImplErrorTest, GlobalDeclConstExprMissingLParen) {
+TEST_F(ParserImplErrorTest, GlobalDeclLetExprMissingLParen) {
     EXPECT("let i : vec2<i32> = vec2<i32> 1, 2);",
            R"(test.wgsl:1:31 error: expected '(' for type constructor
 let i : vec2<i32> = vec2<i32> 1, 2);
@@ -563,7 +664,7 @@
 )");
 }
 
-TEST_F(ParserImplErrorTest, GlobalDeclConstExprMissingRParen) {
+TEST_F(ParserImplErrorTest, GlobalDeclLetExprMissingRParen) {
     EXPECT("let i : vec2<i32> = vec2<i32>(1, 2;",
            R"(test.wgsl:1:35 error: expected ')' for type constructor
 let i : vec2<i32> = vec2<i32>(1, 2;
diff --git a/src/tint/reader/wgsl/parser_impl_global_constant_decl_test.cc b/src/tint/reader/wgsl/parser_impl_global_constant_decl_test.cc
index f5428fc..70caa3f 100644
--- a/src/tint/reader/wgsl/parser_impl_global_constant_decl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_global_constant_decl_test.cc
@@ -18,7 +18,7 @@
 namespace tint::reader::wgsl {
 namespace {
 
-TEST_F(ParserImplTest, GlobalConstantDecl) {
+TEST_F(ParserImplTest, GlobalLetDecl) {
     auto p = parser("let a : f32 = 1.");
     auto attrs = p->attribute_list();
     EXPECT_FALSE(attrs.errored);
@@ -43,7 +43,7 @@
     EXPECT_TRUE(let->constructor->Is<ast::LiteralExpression>());
 }
 
-TEST_F(ParserImplTest, GlobalConstantDecl_Inferred) {
+TEST_F(ParserImplTest, GlobalLetDecl_Inferred) {
     auto p = parser("let a = 1.");
     auto attrs = p->attribute_list();
     EXPECT_FALSE(attrs.errored);
@@ -67,7 +67,7 @@
     EXPECT_TRUE(let->constructor->Is<ast::LiteralExpression>());
 }
 
-TEST_F(ParserImplTest, GlobalConstantDecl_InvalidExpression) {
+TEST_F(ParserImplTest, GlobalLetDecl_InvalidExpression) {
     auto p = parser("let a : f32 = if (a) {}");
     auto attrs = p->attribute_list();
     EXPECT_FALSE(attrs.errored);
@@ -80,7 +80,7 @@
     EXPECT_EQ(p->error(), "1:15: invalid type for const_expr");
 }
 
-TEST_F(ParserImplTest, GlobalConstantDecl_MissingExpression) {
+TEST_F(ParserImplTest, GlobalLetDecl_MissingExpression) {
     auto p = parser("let a : f32 =");
     auto attrs = p->attribute_list();
     EXPECT_FALSE(attrs.errored);
@@ -93,7 +93,82 @@
     EXPECT_EQ(p->error(), "1:14: unable to parse const_expr");
 }
 
-TEST_F(ParserImplTest, GlobalConstantDec_Override_WithId) {
+TEST_F(ParserImplTest, GlobalConstDecl) {
+    auto p = parser("const a : f32 = 1.");
+    auto attrs = p->attribute_list();
+    EXPECT_FALSE(attrs.errored);
+    EXPECT_FALSE(attrs.matched);
+    auto e = p->global_constant_decl(attrs.value);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    EXPECT_TRUE(e.matched);
+    EXPECT_FALSE(e.errored);
+    auto* c = e.value->As<ast::Const>();
+    ASSERT_NE(c, nullptr);
+
+    EXPECT_EQ(c->symbol, p->builder().Symbols().Get("a"));
+    ASSERT_NE(c->type, nullptr);
+    EXPECT_TRUE(c->type->Is<ast::F32>());
+
+    EXPECT_EQ(c->source.range.begin.line, 1u);
+    EXPECT_EQ(c->source.range.begin.column, 7u);
+    EXPECT_EQ(c->source.range.end.line, 1u);
+    EXPECT_EQ(c->source.range.end.column, 8u);
+
+    ASSERT_NE(c->constructor, nullptr);
+    EXPECT_TRUE(c->constructor->Is<ast::LiteralExpression>());
+}
+
+TEST_F(ParserImplTest, GlobalConstDecl_Inferred) {
+    auto p = parser("const a = 1.");
+    auto attrs = p->attribute_list();
+    EXPECT_FALSE(attrs.errored);
+    EXPECT_FALSE(attrs.matched);
+    auto e = p->global_constant_decl(attrs.value);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    EXPECT_TRUE(e.matched);
+    EXPECT_FALSE(e.errored);
+    auto* c = e.value->As<ast::Const>();
+    ASSERT_NE(c, nullptr);
+
+    EXPECT_EQ(c->symbol, p->builder().Symbols().Get("a"));
+    EXPECT_EQ(c->type, nullptr);
+
+    EXPECT_EQ(c->source.range.begin.line, 1u);
+    EXPECT_EQ(c->source.range.begin.column, 7u);
+    EXPECT_EQ(c->source.range.end.line, 1u);
+    EXPECT_EQ(c->source.range.end.column, 8u);
+
+    ASSERT_NE(c->constructor, nullptr);
+    EXPECT_TRUE(c->constructor->Is<ast::LiteralExpression>());
+}
+
+TEST_F(ParserImplTest, GlobalConstDecl_InvalidExpression) {
+    auto p = parser("const a : f32 = if (a) {}");
+    auto attrs = p->attribute_list();
+    EXPECT_FALSE(attrs.errored);
+    EXPECT_FALSE(attrs.matched);
+    auto e = p->global_constant_decl(attrs.value);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_TRUE(e.errored);
+    EXPECT_FALSE(e.matched);
+    EXPECT_EQ(e.value, nullptr);
+    EXPECT_EQ(p->error(), "1:17: invalid type for const_expr");
+}
+
+TEST_F(ParserImplTest, GlobalConstDecl_MissingExpression) {
+    auto p = parser("const a : f32 =");
+    auto attrs = p->attribute_list();
+    EXPECT_FALSE(attrs.errored);
+    EXPECT_FALSE(attrs.matched);
+    auto e = p->global_constant_decl(attrs.value);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_TRUE(e.errored);
+    EXPECT_FALSE(e.matched);
+    EXPECT_EQ(e.value, nullptr);
+    EXPECT_EQ(p->error(), "1:16: unable to parse const_expr");
+}
+
+TEST_F(ParserImplTest, GlobalOverrideDecl_WithId) {
     auto p = parser("@id(7) override a : f32 = 1.");
     auto attrs = p->attribute_list();
     EXPECT_FALSE(attrs.errored);
@@ -123,7 +198,7 @@
     EXPECT_EQ(override_attr->value, 7u);
 }
 
-TEST_F(ParserImplTest, GlobalConstantDec_Override_WithoutId) {
+TEST_F(ParserImplTest, GlobalOverrideDecl_WithoutId) {
     auto p = parser("override a : f32 = 1.");
     auto attrs = p->attribute_list();
     EXPECT_FALSE(attrs.errored);
@@ -152,7 +227,7 @@
     ASSERT_EQ(id_attr, nullptr);
 }
 
-TEST_F(ParserImplTest, GlobalConstantDec_Override_MissingId) {
+TEST_F(ParserImplTest, GlobalOverrideDecl_MissingId) {
     auto p = parser("@id() override a : f32 = 1.");
     auto attrs = p->attribute_list();
     EXPECT_TRUE(attrs.errored);
@@ -168,7 +243,7 @@
     EXPECT_EQ(p->error(), "1:5: expected signed integer literal for id attribute");
 }
 
-TEST_F(ParserImplTest, GlobalConstantDec_Override_InvalidId) {
+TEST_F(ParserImplTest, GlobalOverrideDecl_InvalidId) {
     auto p = parser("@id(-7) override a : f32 = 1.");
     auto attrs = p->attribute_list();
     EXPECT_TRUE(attrs.errored);
diff --git a/src/tint/reader/wgsl/parser_impl_global_decl_test.cc b/src/tint/reader/wgsl/parser_impl_global_decl_test.cc
index 59012a3..a09486d 100644
--- a/src/tint/reader/wgsl/parser_impl_global_decl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_global_decl_test.cc
@@ -49,7 +49,7 @@
     EXPECT_EQ(p->error(), "1:27: expected ';' for variable declaration");
 }
 
-TEST_F(ParserImplTest, GlobalDecl_GlobalConstant) {
+TEST_F(ParserImplTest, GlobalDecl_GlobalLet) {
     auto p = parser("let a : i32 = 2;");
     p->global_decl();
     ASSERT_FALSE(p->has_error()) << p->error();
@@ -61,27 +61,60 @@
     EXPECT_EQ(v->symbol, program.Symbols().Get("a"));
 }
 
-TEST_F(ParserImplTest, GlobalDecl_GlobalConstant_MissingInitializer) {
+TEST_F(ParserImplTest, GlobalDecl_GlobalLet_MissingInitializer) {
     auto p = parser("let a : vec2<i32>;");
     p->global_decl();
     ASSERT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), "1:18: expected '=' for 'let' declaration");
 }
 
-TEST_F(ParserImplTest, GlobalDecl_GlobalConstant_Invalid) {
+TEST_F(ParserImplTest, GlobalDecl_GlobalLet_Invalid) {
     auto p = parser("let a : vec2<i32> 1.0;");
     p->global_decl();
     ASSERT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), "1:19: expected '=' for 'let' declaration");
 }
 
-TEST_F(ParserImplTest, GlobalDecl_GlobalConstant_MissingSemicolon) {
+TEST_F(ParserImplTest, GlobalDecl_GlobalLet_MissingSemicolon) {
     auto p = parser("let a : vec2<i32> = vec2<i32>(1, 2)");
     p->global_decl();
     ASSERT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), "1:36: expected ';' for 'let' declaration");
 }
 
+TEST_F(ParserImplTest, GlobalDecl_GlobalConst) {
+    auto p = parser("const a : i32 = 2;");
+    p->global_decl();
+    ASSERT_FALSE(p->has_error()) << p->error();
+
+    auto program = p->program();
+    ASSERT_EQ(program.AST().GlobalVariables().size(), 1u);
+
+    auto* v = program.AST().GlobalVariables()[0];
+    EXPECT_EQ(v->symbol, program.Symbols().Get("a"));
+}
+
+TEST_F(ParserImplTest, GlobalDecl_GlobalConst_MissingInitializer) {
+    auto p = parser("const a : vec2<i32>;");
+    p->global_decl();
+    ASSERT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:20: expected '=' for 'const' declaration");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_GlobalConst_Invalid) {
+    auto p = parser("const a : vec2<i32> 1.0;");
+    p->global_decl();
+    ASSERT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:21: expected '=' for 'const' declaration");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_GlobalConst_MissingSemicolon) {
+    auto p = parser("const a : vec2<i32> = vec2<i32>(1, 2)");
+    p->global_decl();
+    ASSERT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:38: expected ';' for 'const' declaration");
+}
+
 TEST_F(ParserImplTest, GlobalDecl_TypeAlias) {
     auto p = parser("type A = i32;");
     p->global_decl();
diff --git a/src/tint/reader/wgsl/parser_impl_reserved_keyword_test.cc b/src/tint/reader/wgsl/parser_impl_reserved_keyword_test.cc
index 8f2d470..65fae3c 100644
--- a/src/tint/reader/wgsl/parser_impl_reserved_keyword_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_reserved_keyword_test.cc
@@ -32,6 +32,13 @@
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), "1:5: '" + name + "' is a reserved keyword");
 }
+TEST_P(ParserImplReservedKeywordTest, ModuleConst) {
+    auto name = GetParam();
+    auto p = parser("const " + name + " : i32 = 1;");
+    EXPECT_FALSE(p->Parse());
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:7: '" + name + "' is a reserved keyword");
+}
 TEST_P(ParserImplReservedKeywordTest, ModuleVar) {
     auto name = GetParam();
     auto p = parser("var " + name + " : i32 = 1;");
diff --git a/src/tint/resolver/dependency_graph.cc b/src/tint/resolver/dependency_graph.cc
index ec450ec..be4511b 100644
--- a/src/tint/resolver/dependency_graph.cc
+++ b/src/tint/resolver/dependency_graph.cc
@@ -487,14 +487,11 @@
     /// declaration
     std::string KindOf(const ast::Node* node) {
         return Switch(
-            node,                                              //
-            [&](const ast::Struct*) { return "struct"; },      //
-            [&](const ast::Alias*) { return "alias"; },        //
-            [&](const ast::Function*) { return "function"; },  //
-            [&](const ast::Var*) { return "var"; },            //
-            [&](const ast::Let*) { return "let"; },            //
-            [&](const ast::Override*) { return "override"; },  //
-            [&](const ast::Const*) { return "const"; },        //
+            node,                                               //
+            [&](const ast::Struct*) { return "struct"; },       //
+            [&](const ast::Alias*) { return "alias"; },         //
+            [&](const ast::Function*) { return "function"; },   //
+            [&](const ast::Variable* v) { return v->Kind(); },  //
             [&](Default) {
                 UnhandledNode(diagnostics_, node);
                 return "<error>";
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index 5676401..6ffbd62 100644
--- a/src/tint/resolver/validator.cc
+++ b/src/tint/resolver/validator.cc
@@ -327,16 +327,10 @@
 
     // Value type has to match storage type
     if (storage_ty != value_type) {
-        std::string decl = Switch(
-            v,                                           //
-            [&](const ast::Var*) { return "var"; },      //
-            [&](const ast::Let*) { return "let"; },      //
-            [&](const ast::Const*) { return "const"; },  //
-            [&](Default) { return "<unknown>"; });
-
-        AddError("cannot initialize " + decl + " of type '" + sem_.TypeNameOf(storage_ty) +
-                     "' with value of type '" + sem_.TypeNameOf(rhs_ty) + "'",
-                 v->source);
+        std::stringstream s;
+        s << "cannot initialize " << v->Kind() << " of type '" << sem_.TypeNameOf(storage_ty)
+          << "' with value of type '" << sem_.TypeNameOf(rhs_ty) << "'";
+        AddError(s.str(), v->source);
         return false;
     }
 
diff --git a/src/tint/test_main.cc b/src/tint/test_main.cc
index ca68271..0114d4b 100644
--- a/src/tint/test_main.cc
+++ b/src/tint/test_main.cc
@@ -15,6 +15,11 @@
 #include "gmock/gmock.h"
 #include "src/tint/program.h"
 
+// TODO(crbug.com/tint/1580): Remove when 'const' is fully implemented.
+#if TINT_BUILD_WGSL_READER
+#include "src/tint/reader/wgsl/parser_impl.h"
+#endif
+
 #if TINT_BUILD_SPV_READER
 #include "src/tint/reader/spirv/parser_impl_test_helper.h"
 #endif
@@ -54,6 +59,11 @@
 int main(int argc, char** argv) {
     testing::InitGoogleMock(&argc, argv);
 
+// TODO(crbug.com/tint/1580): Remove when 'const' is fully implemented.
+#if TINT_BUILD_WGSL_READER
+    tint::reader::wgsl::ParserImpl::EnableConst();
+#endif
+
 #if TINT_BUILD_WGSL_WRITER
     tint::Program::printer = [](const tint::Program* program) {
         auto result = tint::writer::wgsl::Generate(program, {});