tint/reader: Parse static_assert

Bug: tint:1625
Change-Id: I440de9a5f0f6817520d59d0d918acc6ac0555c42
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/97963
Commit-Queue: Ben Clayton <bclayton@chromium.org>
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
diff --git a/src/tint/reader/wgsl/lexer.cc b/src/tint/reader/wgsl/lexer.cc
index 2b23a0c..0bc19ad 100644
--- a/src/tint/reader/wgsl/lexer.cc
+++ b/src/tint/reader/wgsl/lexer.cc
@@ -1243,6 +1243,9 @@
     if (str == "sampler_comparison") {
         return {Token::Type::kComparisonSampler, source, "sampler_comparison"};
     }
+    if (str == "static_assert") {
+        return {Token::Type::kStaticAssert, source, "static_assert"};
+    }
     if (str == "struct") {
         return {Token::Type::kStruct, source, "struct"};
     }
diff --git a/src/tint/reader/wgsl/lexer_test.cc b/src/tint/reader/wgsl/lexer_test.cc
index f1818ab..6cac798 100644
--- a/src/tint/reader/wgsl/lexer_test.cc
+++ b/src/tint/reader/wgsl/lexer_test.cc
@@ -1145,6 +1145,7 @@
                     TokenData{"return", Token::Type::kReturn},
                     TokenData{"sampler", Token::Type::kSampler},
                     TokenData{"sampler_comparison", Token::Type::kComparisonSampler},
+                    TokenData{"static_assert", Token::Type::kStaticAssert},
                     TokenData{"struct", Token::Type::kStruct},
                     TokenData{"switch", Token::Type::kSwitch},
                     TokenData{"texture_1d", Token::Type::kTextureSampled1d},
diff --git a/src/tint/reader/wgsl/parser_impl.cc b/src/tint/reader/wgsl/parser_impl.cc
index 57cfd11..459a21f 100644
--- a/src/tint/reader/wgsl/parser_impl.cc
+++ b/src/tint/reader/wgsl/parser_impl.cc
@@ -111,13 +111,13 @@
            t == "readonly" || t == "ref" || t == "regardless" || t == "register" ||
            t == "reinterpret_cast" || t == "requires" || t == "resource" || t == "restrict" ||
            t == "self" || t == "set" || t == "shared" || t == "signed" || t == "sizeof" ||
-           t == "smooth" || t == "snorm" || t == "static" || t == "static_assert" ||
-           t == "static_cast" || t == "std" || t == "subroutine" || t == "super" || t == "target" ||
-           t == "template" || t == "this" || t == "thread_local" || t == "throw" || t == "trait" ||
-           t == "try" || t == "typedef" || t == "typeid" || t == "typename" || t == "typeof" ||
-           t == "union" || t == "unless" || t == "unorm" || t == "unsafe" || t == "unsized" ||
-           t == "use" || t == "using" || t == "varying" || t == "virtual" || t == "volatile" ||
-           t == "wgsl" || t == "where" || t == "with" || t == "writeonly" || t == "yield";
+           t == "smooth" || t == "snorm" || t == "static" || t == "static_cast" || t == "std" ||
+           t == "subroutine" || t == "super" || t == "target" || t == "template" || t == "this" ||
+           t == "thread_local" || t == "throw" || t == "trait" || t == "try" || t == "typedef" ||
+           t == "typeid" || t == "typename" || t == "typeof" || t == "union" || t == "unless" ||
+           t == "unorm" || t == "unsafe" || t == "unsized" || t == "use" || t == "using" ||
+           t == "varying" || t == "virtual" || t == "volatile" || t == "wgsl" || t == "where" ||
+           t == "with" || t == "writeonly" || t == "yield";
 }
 
 /// Enter-exit counters for block token types.
@@ -438,11 +438,12 @@
 
 // global_decl
 //  : SEMICOLON
-//  | global_variable_decl SEMICLON
+//  | global_variable_decl SEMICOLON
 //  | global_constant_decl SEMICOLON
 //  | type_alias SEMICOLON
 //  | struct_decl
 //  | function_decl
+//  | static_assert_statement SEMICOLON
 Maybe<bool> ParserImpl::global_decl() {
     if (match(Token::Type::kSemicolon) || match(Token::Type::kEOF)) {
         return true;
@@ -476,7 +477,6 @@
         if (gc.errored) {
             return Failure::kErrored;
         }
-
         if (gc.matched) {
             // Avoid the cost of the string allocation for the common no-error case
             if (!peek().Is(Token::Type::kSemicolon)) {
@@ -494,7 +494,6 @@
         if (ta.errored) {
             return Failure::kErrored;
         }
-
         if (ta.matched) {
             if (!expect("type alias", Token::Type::kSemicolon)) {
                 return Failure::kErrored;
@@ -508,12 +507,23 @@
         if (str.errored) {
             return Failure::kErrored;
         }
-
         if (str.matched) {
             builder_.AST().AddTypeDecl(str.value);
             return true;
         }
 
+        auto assertion = static_assert_stmt();
+        if (assertion.errored) {
+            return Failure::kErrored;
+        }
+        if (assertion.matched) {
+            builder_.AST().AddStaticAssert(assertion.value);
+            if (!expect("static assertion declaration", Token::Type::kSemicolon)) {
+                return Failure::kErrored;
+            }
+            return true;
+        }
+
         return Failure::kNoMatch;
     });
 
@@ -1365,6 +1375,24 @@
                                      decl->type, std::move(attrs.value));
 }
 
+Maybe<const ast::StaticAssert*> ParserImpl::static_assert_stmt() {
+    Source start;
+    if (!match(Token::Type::kStaticAssert, &start)) {
+        return Failure::kNoMatch;
+    }
+
+    auto condition = logical_or_expression();
+    if (condition.errored) {
+        return Failure::kErrored;
+    }
+    if (!condition.matched) {
+        return add_error(peek(), "unable to parse condition expression");
+    }
+
+    Source source = make_source_range_from(start);
+    return create<ast::StaticAssert>(source, condition.value);
+}
+
 // function_decl
 //   : function_header body_stmt
 Maybe<const ast::Function*> ParserImpl::function_decl(AttributeList& attrs) {
@@ -1613,12 +1641,13 @@
 //      | assignment_stmt SEMICOLON
 //      | increment_stmt SEMICOLON
 //      | decrement_stmt SEMICOLON
+//      | static_assert_stmt SEMICOLON
 Maybe<const ast::Statement*> ParserImpl::statement() {
     while (match(Token::Type::kSemicolon)) {
         // Skip empty statements
     }
 
-    // Non-block statments that error can resynchronize on semicolon.
+    // Non-block statements that error can resynchronize on semicolon.
     auto stmt = sync(Token::Type::kSemicolon, [&] { return non_block_statement(); });
 
     if (stmt.errored) {
@@ -1739,6 +1768,14 @@
             return assign.value;
         }
 
+        auto stmt_static_assert = static_assert_stmt();
+        if (stmt_static_assert.errored) {
+            return Failure::kErrored;
+        }
+        if (stmt_static_assert.matched) {
+            return stmt_static_assert.value;
+        }
+
         Source source;
         if (match(Token::Type::kDiscard, &source)) {
             return create<ast::DiscardStatement>(source);
diff --git a/src/tint/reader/wgsl/parser_impl.h b/src/tint/reader/wgsl/parser_impl.h
index 6dfc13f..63e1499 100644
--- a/src/tint/reader/wgsl/parser_impl.h
+++ b/src/tint/reader/wgsl/parser_impl.h
@@ -472,6 +472,9 @@
     /// @param use a description of what was being parsed if an error was raised
     /// @returns returns the texel format or kNone if none matched.
     Expect<ast::TexelFormat> expect_texel_format(std::string_view use);
+    /// Parses a `static_assert_statement` grammar element
+    /// @returns returns the static assert, if it matched.
+    Maybe<const ast::StaticAssert*> static_assert_stmt();
     /// Parses a `function_header` grammar element
     /// @returns the parsed function header
     Maybe<FunctionHeader> function_header();
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 81613b4..85e888d 100644
--- a/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
@@ -306,6 +306,51 @@
 )");
 }
 
+TEST_F(ParserImplErrorTest, FunctionDeclStaticAssertMissingCondThenEOF) {
+    EXPECT("fn f() { static_assert }", R"(test.wgsl:1:24 error: unable to parse condition expression
+fn f() { static_assert }
+                       ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, FunctionDeclStaticAssertMissingCondThenSemicolon) {
+    EXPECT("fn f() { static_assert; }",
+           R"(test.wgsl:1:23 error: unable to parse condition expression
+fn f() { static_assert; }
+                      ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, FunctionDeclStaticAssertMissingCondThenLet) {
+    EXPECT("fn f() { static_assert\nlet x = 0; }",
+           R"(test.wgsl:2:1 error: unable to parse condition expression
+let x = 0; }
+^^^
+)");
+}
+
+TEST_F(ParserImplErrorTest, FunctionDeclStaticAssertMissingLParen) {
+    EXPECT("fn f() { static_assert true);", R"(test.wgsl:1:28 error: expected ';' for statement
+fn f() { static_assert true);
+                           ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, FunctionDeclStaticAssertMissingRParen) {
+    EXPECT("fn f() { static_assert (true;", R"(test.wgsl:1:29 error: expected ')'
+fn f() { static_assert (true;
+                            ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, FunctionDeclStaticAssertMissingSemicolon) {
+    EXPECT("fn f() { static_assert true }",
+           R"(test.wgsl:1:29 error: expected ';' for statement
+fn f() { static_assert true }
+                            ^
+)");
+}
+
 // TODO(crbug.com/tint/1503): Remove this when @stage is removed
 TEST_F(ParserImplErrorTest, FunctionDeclStageMissingLParen) {
     EXPECT("@stage vertex) fn f() {}",
@@ -697,6 +742,50 @@
 )");
 }
 
+TEST_F(ParserImplErrorTest, GlobalDeclStaticAssertMissingCondThenEOF) {
+    EXPECT("static_assert", R"(test.wgsl:1:14 error: unable to parse condition expression
+static_assert
+             ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclStaticAssertMissingCondThenSemicolon) {
+    EXPECT("static_assert;", R"(test.wgsl:1:14 error: unable to parse condition expression
+static_assert;
+             ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclStaticAssertMissingCondThenAlias) {
+    EXPECT("static_assert\ntype T = i32;",
+           R"(test.wgsl:2:1 error: unable to parse condition expression
+type T = i32;
+^^^^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclStaticAssertMissingLParen) {
+    EXPECT("static_assert true);", R"(test.wgsl:1:19 error: expected ';' for static assertion declaration
+static_assert true);
+                  ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclStaticAssertMissingRParen) {
+    EXPECT("static_assert (true;", R"(test.wgsl:1:20 error: expected ')'
+static_assert (true;
+                   ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclStaticAssertMissingSemicolon) {
+    EXPECT("static_assert true static_assert true;",
+           R"(test.wgsl:1:20 error: expected ';' for static assertion declaration
+static_assert true static_assert true;
+                   ^^^^^^^^^^^^^
+)");
+}
+
 TEST_F(ParserImplErrorTest, GlobalDeclStorageTextureMissingLessThan) {
     EXPECT("var x : texture_storage_2d;",
            R"(test.wgsl:1:27 error: expected '<' for storage texture type
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 020e515..d9001c5 100644
--- a/src/tint/reader/wgsl/parser_impl_global_decl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_global_decl_test.cc
@@ -233,5 +233,47 @@
     }
 }
 
+TEST_F(ParserImplTest, GlobalDecl_StaticAssert_WithParen) {
+    auto p = parser("static_assert(true);");
+    p->global_decl();
+    ASSERT_FALSE(p->has_error()) << p->error();
+
+    auto program = p->program();
+    ASSERT_EQ(program.AST().StaticAsserts().Length(), 1u);
+    auto* sa = program.AST().StaticAsserts()[0];
+    EXPECT_EQ(sa->source.range.begin.line, 1u);
+    EXPECT_EQ(sa->source.range.begin.column, 1u);
+    EXPECT_EQ(sa->source.range.end.line, 1u);
+    EXPECT_EQ(sa->source.range.end.column, 20u);
+
+    EXPECT_TRUE(sa->condition->Is<ast::BoolLiteralExpression>());
+    EXPECT_EQ(sa->condition->source.range.begin.line, 1u);
+    EXPECT_EQ(sa->condition->source.range.begin.column, 15u);
+    EXPECT_EQ(sa->condition->source.range.end.line, 1u);
+    EXPECT_EQ(sa->condition->source.range.end.column, 19u);
+}
+
+TEST_F(ParserImplTest, GlobalDecl_StaticAssert_WithoutParen) {
+    auto p = parser("static_assert  true;");
+    p->global_decl();
+    ASSERT_FALSE(p->has_error()) << p->error();
+
+    auto program = p->program();
+    ASSERT_EQ(program.AST().StaticAsserts().Length(), 1u);
+    auto* sa = program.AST().StaticAsserts()[0];
+    EXPECT_TRUE(sa->condition->Is<ast::BoolLiteralExpression>());
+
+    EXPECT_EQ(sa->source.range.begin.line, 1u);
+    EXPECT_EQ(sa->source.range.begin.column, 1u);
+    EXPECT_EQ(sa->source.range.end.line, 1u);
+    EXPECT_EQ(sa->source.range.end.column, 20u);
+
+    EXPECT_TRUE(sa->condition->Is<ast::BoolLiteralExpression>());
+    EXPECT_EQ(sa->condition->source.range.begin.line, 1u);
+    EXPECT_EQ(sa->condition->source.range.begin.column, 16u);
+    EXPECT_EQ(sa->condition->source.range.end.line, 1u);
+    EXPECT_EQ(sa->condition->source.range.end.column, 20u);
+}
+
 }  // namespace
 }  // namespace tint::reader::wgsl
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 07a88cd..0ad3cc1 100644
--- a/src/tint/reader/wgsl/parser_impl_reserved_keyword_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_reserved_keyword_test.cc
@@ -215,7 +215,6 @@
                                          "smooth",
                                          "snorm",
                                          "static",
-                                         "static_assert",
                                          "static_cast",
                                          "std",
                                          "subroutine",
diff --git a/src/tint/reader/wgsl/parser_impl_statement_test.cc b/src/tint/reader/wgsl/parser_impl_statement_test.cc
index 677daa0..7bb51d3 100644
--- a/src/tint/reader/wgsl/parser_impl_statement_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_statement_test.cc
@@ -272,5 +272,47 @@
     EXPECT_EQ(p->error(), "1:3: expected '}'");
 }
 
+TEST_F(ParserImplTest, Statement_StaticAssert_WithParen) {
+    auto p = parser("static_assert(true);");
+    auto e = p->statement();
+    ASSERT_FALSE(p->has_error()) << p->error();
+    EXPECT_TRUE(e.matched);
+    EXPECT_FALSE(e.errored);
+
+    auto* sa = As<ast::StaticAssert>(e.value);
+    ASSERT_NE(sa, nullptr);
+    EXPECT_EQ(sa->source.range.begin.line, 1u);
+    EXPECT_EQ(sa->source.range.begin.column, 1u);
+    EXPECT_EQ(sa->source.range.end.line, 1u);
+    EXPECT_EQ(sa->source.range.end.column, 20u);
+
+    EXPECT_TRUE(sa->condition->Is<ast::BoolLiteralExpression>());
+    EXPECT_EQ(sa->condition->source.range.begin.line, 1u);
+    EXPECT_EQ(sa->condition->source.range.begin.column, 15u);
+    EXPECT_EQ(sa->condition->source.range.end.line, 1u);
+    EXPECT_EQ(sa->condition->source.range.end.column, 19u);
+}
+
+TEST_F(ParserImplTest, Statement_StaticAssert_WithoutParen) {
+    auto p = parser("static_assert  true;");
+    auto e = p->statement();
+    ASSERT_FALSE(p->has_error()) << p->error();
+    EXPECT_TRUE(e.matched);
+    EXPECT_FALSE(e.errored);
+
+    auto* sa = As<ast::StaticAssert>(e.value);
+    ASSERT_NE(sa, nullptr);
+    EXPECT_EQ(sa->source.range.begin.line, 1u);
+    EXPECT_EQ(sa->source.range.begin.column, 1u);
+    EXPECT_EQ(sa->source.range.end.line, 1u);
+    EXPECT_EQ(sa->source.range.end.column, 20u);
+
+    EXPECT_TRUE(sa->condition->Is<ast::BoolLiteralExpression>());
+    EXPECT_EQ(sa->condition->source.range.begin.line, 1u);
+    EXPECT_EQ(sa->condition->source.range.begin.column, 16u);
+    EXPECT_EQ(sa->condition->source.range.end.line, 1u);
+    EXPECT_EQ(sa->condition->source.range.end.column, 20u);
+}
+
 }  // namespace
 }  // namespace tint::reader::wgsl
diff --git a/src/tint/reader/wgsl/token.cc b/src/tint/reader/wgsl/token.cc
index 777492c..ac5eb08 100644
--- a/src/tint/reader/wgsl/token.cc
+++ b/src/tint/reader/wgsl/token.cc
@@ -209,6 +209,8 @@
             return "sampler";
         case Token::Type::kComparisonSampler:
             return "sampler_comparison";
+        case Token::Type::kStaticAssert:
+            return "static_assert";
         case Token::Type::kStruct:
             return "struct";
         case Token::Type::kSwitch:
diff --git a/src/tint/reader/wgsl/token.h b/src/tint/reader/wgsl/token.h
index 4f8a9d6..68cb6c6 100644
--- a/src/tint/reader/wgsl/token.h
+++ b/src/tint/reader/wgsl/token.h
@@ -219,6 +219,8 @@
         kSampler,
         /// A 'sampler_comparison'
         kComparisonSampler,
+        /// A 'static_assert'
+        kStaticAssert,
         /// A 'struct'
         kStruct,
         /// A 'switch'