Import Tint changes from Dawn

Changes:
  - bb0229002db5cd1c58df4ff846020dcc180f9258 tint/uniformity: Violations are now errors by James Price <jrprice@google.com>
  - f38ee3e07392eb1d66b8f9fb0702ccef3d062926 tint/exe: Add --allow-non-uniform-derivatives flag by James Price <jrprice@google.com>
  - e657c470bdc66ba10468ed494a4cda0a7258f982 tint/uniformity: Add derivative_uniformity filter by James Price <jrprice@google.com>
  - 5853205342386f296377f3276a76d36181558ca3 tint/sem: Record diagnostic severity modifications by James Price <jrprice@google.com>
  - 613fbcf79ad0b366711f144dbd1d1c07132b910d tint: Add diagnostic and enable to ModuleCloneTest by James Price <jrprice@google.com>
  - c744a23d77d53b8e8e7b9d5c7d7439e2622034da tint/reader/wgsl: Parse @diagnostic attributes by James Price <jrprice@google.com>
  - 75326f88e64cad9bbf3e469e61ac1427fc8f43d8 tint/writer/wgsl: Handle diagnostic attributes by James Price <jrprice@google.com>
  - 8dd35110c28830d2d6f81145b142860d57bfc845 wgsl/reader: Parse diagnostic directives by James Price <jrprice@google.com>
  - 02855b5060831b7fa2dedf1a62567a3dc0c6207b tint/resolver: Handle diagnostic attributes on functions by James Price <jrprice@google.com>
  - e8ea579bceafe593964e6110559925362eb80842 tint/writer: Handle diagnostic directives by James Price <jrprice@google.com>
  - 81e55754e148eb27ec835fceebac577003df8a6b tint/resolver: Handle diagnostic directives by James Price <jrprice@google.com>
  - 098e3d8f90800a8a2f1f0a5aff054fcdb4bca146 tint/dependency_graph: Handle diagnostic controls by James Price <jrprice@google.com>
  - 15bf15d55f5f90e08c912d70be5de207cd34a80f tint/ast: Add global diagnostic controls to Module by James Price <jrprice@google.com>
  - 7d2b192439cb4a0380a2eb60947540a79a157847 tint: rename template argument token strings by Ben Clayton <bclayton@google.com>
  - f0d8edecee77d05fb3e670ca5a77ad211116e78a tint: Fix x86 build by Ben Clayton <bclayton@google.com>
  - 355b70d2be291a5d069df330810d0b484592618d tint/reader/wgsl: Use ClassifyTemplateArguments() by Ben Clayton <bclayton@google.com>
  - ef5434dcd17a18369084fc8b6b315bf3ddb609e8 tint/ast: Add DiagnosticAttribute AST node by James Price <jrprice@google.com>
  - 4ff2645d1620d65aae6d847f191ae0ca3ee27e22 tint/reader/wgsl: Add ClassifyTemplateArguments() by Ben Clayton <bclayton@google.com>
  - 6c8aa8aff9d13050080ce3e7c471d5a72273ade7 Resolve some reader/writer dependency issues by Brandon Jones <bajones@chromium.org>
  - c98d57d662b7fdd0c82fa8169cd98f87007530be tint: Rename 'static_assert' to 'const_assert' by Ben Clayton <bclayton@google.com>
  - 9dc48bcef3c390ac55eb52b76f932092c7c79f0a tint: Rename 'type' to 'alias' by Ben Clayton <bclayton@google.com>
GitOrigin-RevId: bb0229002db5cd1c58df4ff846020dcc180f9258
Change-Id: I2d8e330bd6e37c5bd974a4bafa259a3aa947c163
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/117530
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 9a14d8a..0473d49 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -224,12 +224,16 @@
     "ast/compound_assignment_statement.h",
     "ast/const.cc",
     "ast/const.h",
+    "ast/const_assert.cc",
+    "ast/const_assert.h",
     "ast/continue_statement.cc",
     "ast/continue_statement.h",
     "ast/depth_multisampled_texture.cc",
     "ast/depth_multisampled_texture.h",
     "ast/depth_texture.cc",
     "ast/depth_texture.h",
+    "ast/diagnostic_attribute.cc",
+    "ast/diagnostic_attribute.h",
     "ast/diagnostic_control.cc",
     "ast/diagnostic_control.h",
     "ast/disable_validation_attribute.cc",
@@ -315,8 +319,6 @@
     "ast/stage_attribute.h",
     "ast/statement.cc",
     "ast/statement.h",
-    "ast/static_assert.cc",
-    "ast/static_assert.h",
     "ast/storage_texture.cc",
     "ast/storage_texture.h",
     "ast/stride_attribute.cc",
@@ -908,6 +910,8 @@
 
 libtint_source_set("libtint_wgsl_reader_src") {
   sources = [
+    "reader/wgsl/classify_template_args.cc",
+    "reader/wgsl/classify_template_args.h",
     "reader/wgsl/lexer.cc",
     "reader/wgsl/lexer.h",
     "reader/wgsl/parser.cc",
@@ -1179,7 +1183,9 @@
       "ast/case_selector_test.cc",
       "ast/case_statement_test.cc",
       "ast/compound_assignment_statement_test.cc",
+      "ast/const_assert_test.cc",
       "ast/continue_statement_test.cc",
+      "ast/diagnostic_attribute_test.cc",
       "ast/diagnostic_control_test.cc",
       "ast/discard_statement_test.cc",
       "ast/enable_test.cc",
@@ -1204,7 +1210,6 @@
       "ast/loop_statement_test.cc",
       "ast/matrix_test.cc",
       "ast/member_accessor_expression_test.cc",
-      "ast/module_clone_test.cc",
       "ast/module_test.cc",
       "ast/multisampled_texture_test.cc",
       "ast/phony_expression_test.cc",
@@ -1213,7 +1218,6 @@
       "ast/sampled_texture_test.cc",
       "ast/sampler_test.cc",
       "ast/stage_attribute_test.cc",
-      "ast/static_assert_test.cc",
       "ast/storage_texture_test.cc",
       "ast/stride_attribute_test.cc",
       "ast/struct_member_align_attribute_test.cc",
@@ -1234,6 +1238,11 @@
       "ast/workgroup_attribute_test.cc",
     ]
     deps = [ ":libtint_transform_src" ]
+
+    if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
+      # This AST test relies on the WGSL reader and writer
+      sources += [ "ast/module_clone_test.cc" ]
+    }
   }
 
   tint_unittests_source_set("tint_unittests_diagnostic_src") {
@@ -1272,6 +1281,7 @@
       "resolver/call_validation_test.cc",
       "resolver/compound_assignment_validation_test.cc",
       "resolver/compound_statement_test.cc",
+      "resolver/const_assert_test.cc",
       "resolver/const_eval_binary_op_test.cc",
       "resolver/const_eval_bitcast_test.cc",
       "resolver/const_eval_builtin_test.cc",
@@ -1283,6 +1293,7 @@
       "resolver/const_eval_unary_op_test.cc",
       "resolver/control_block_validation_test.cc",
       "resolver/dependency_graph_test.cc",
+      "resolver/diagnostic_control_test.cc",
       "resolver/entry_point_validation_test.cc",
       "resolver/evaluation_stage_test.cc",
       "resolver/f16_extension_test.cc",
@@ -1304,7 +1315,6 @@
       "resolver/resolver_test_helper.h",
       "resolver/root_identifier_test.cc",
       "resolver/side_effects_test.cc",
-      "resolver/static_assert_test.cc",
       "resolver/struct_address_space_use_test.cc",
       "resolver/struct_layout_test.cc",
       "resolver/struct_pipeline_stage_use_test.cc",
@@ -1325,6 +1335,7 @@
   tint_unittests_source_set("tint_unittests_sem_src") {
     sources = [
       "sem/builtin_test.cc",
+      "sem/diagnostic_severity_test.cc",
       "sem/expression_test.cc",
       "sem/struct_test.cc",
     ]
@@ -1432,7 +1443,11 @@
       "transform/zero_init_workgroup_memory_test.cc",
     ]
 
-    deps = [ ":libtint_transform_src" ]
+    deps = [
+      ":libtint_transform_src",
+      ":libtint_wgsl_reader_src",
+      ":libtint_wgsl_writer_src",
+    ]
   }
 
   tint_unittests_source_set("tint_unittests_utils_src") {
@@ -1511,6 +1526,7 @@
 
     deps = [
       ":libtint_spv_reader_src",
+      ":libtint_wgsl_writer_src",
       "${tint_spirv_tools_dir}/:spvtools_opt",
     ]
   }
@@ -1526,6 +1542,7 @@
       "writer/spirv/builder_builtin_test.cc",
       "writer/spirv/builder_builtin_texture_test.cc",
       "writer/spirv/builder_call_test.cc",
+      "writer/spirv/builder_const_assert_test.cc",
       "writer/spirv/builder_discard_test.cc",
       "writer/spirv/builder_entry_point_test.cc",
       "writer/spirv/builder_format_conversion_test.cc",
@@ -1539,7 +1556,6 @@
       "writer/spirv/builder_literal_test.cc",
       "writer/spirv/builder_loop_test.cc",
       "writer/spirv/builder_return_test.cc",
-      "writer/spirv/builder_static_assert_test.cc",
       "writer/spirv/builder_switch_test.cc",
       "writer/spirv/builder_test.cc",
       "writer/spirv/builder_type_test.cc",
@@ -1561,6 +1577,7 @@
 
   tint_unittests_source_set("tint_unittests_wgsl_reader_src") {
     sources = [
+      "reader/wgsl/classify_template_args_test.cc",
       "reader/wgsl/lexer_test.cc",
       "reader/wgsl/parser_impl_additive_expression_test.cc",
       "reader/wgsl/parser_impl_address_space_test.cc",
@@ -1578,6 +1595,9 @@
       "reader/wgsl/parser_impl_continuing_stmt_test.cc",
       "reader/wgsl/parser_impl_core_lhs_expression_test.cc",
       "reader/wgsl/parser_impl_depth_texture_test.cc",
+      "reader/wgsl/parser_impl_diagnostic_attribute_test.cc",
+      "reader/wgsl/parser_impl_diagnostic_control_test.cc",
+      "reader/wgsl/parser_impl_diagnostic_directive_test.cc",
       "reader/wgsl/parser_impl_element_count_expression_test.cc",
       "reader/wgsl/parser_impl_enable_directive_test.cc",
       "reader/wgsl/parser_impl_error_msg_test.cc",
@@ -1654,7 +1674,9 @@
       "writer/wgsl/generator_impl_call_test.cc",
       "writer/wgsl/generator_impl_case_test.cc",
       "writer/wgsl/generator_impl_cast_test.cc",
+      "writer/wgsl/generator_impl_const_assert_test.cc",
       "writer/wgsl/generator_impl_continue_test.cc",
+      "writer/wgsl/generator_impl_diagnostic_test.cc",
       "writer/wgsl/generator_impl_discard_test.cc",
       "writer/wgsl/generator_impl_enable_test.cc",
       "writer/wgsl/generator_impl_function_test.cc",
@@ -1666,7 +1688,6 @@
       "writer/wgsl/generator_impl_loop_test.cc",
       "writer/wgsl/generator_impl_member_accessor_test.cc",
       "writer/wgsl/generator_impl_return_test.cc",
-      "writer/wgsl/generator_impl_static_assert_test.cc",
       "writer/wgsl/generator_impl_switch_test.cc",
       "writer/wgsl/generator_impl_test.cc",
       "writer/wgsl/generator_impl_type_test.cc",
@@ -1695,6 +1716,7 @@
       "writer/msl/generator_impl_call_test.cc",
       "writer/msl/generator_impl_case_test.cc",
       "writer/msl/generator_impl_cast_test.cc",
+      "writer/msl/generator_impl_const_assert_test.cc",
       "writer/msl/generator_impl_continue_test.cc",
       "writer/msl/generator_impl_discard_test.cc",
       "writer/msl/generator_impl_function_test.cc",
@@ -1707,7 +1729,6 @@
       "writer/msl/generator_impl_module_constant_test.cc",
       "writer/msl/generator_impl_return_test.cc",
       "writer/msl/generator_impl_sanitizer_test.cc",
-      "writer/msl/generator_impl_static_assert_test.cc",
       "writer/msl/generator_impl_switch_test.cc",
       "writer/msl/generator_impl_test.cc",
       "writer/msl/generator_impl_type_test.cc",
@@ -1735,6 +1756,7 @@
       "writer/hlsl/generator_impl_call_test.cc",
       "writer/hlsl/generator_impl_case_test.cc",
       "writer/hlsl/generator_impl_cast_test.cc",
+      "writer/hlsl/generator_impl_const_assert_test.cc",
       "writer/hlsl/generator_impl_continue_test.cc",
       "writer/hlsl/generator_impl_discard_test.cc",
       "writer/hlsl/generator_impl_function_test.cc",
@@ -1747,7 +1769,6 @@
       "writer/hlsl/generator_impl_module_constant_test.cc",
       "writer/hlsl/generator_impl_return_test.cc",
       "writer/hlsl/generator_impl_sanitizer_test.cc",
-      "writer/hlsl/generator_impl_static_assert_test.cc",
       "writer/hlsl/generator_impl_switch_test.cc",
       "writer/hlsl/generator_impl_test.cc",
       "writer/hlsl/generator_impl_type_test.cc",
@@ -1804,7 +1825,6 @@
       ":libtint_glsl_writer_src",
       ":libtint_transform_src",
       ":tint_unittests_ast_src",
-      ":tint_unittests_transform_src",
     ]
   }
 
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 438ab85..58d28cf 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -119,12 +119,16 @@
   ast/compound_assignment_statement.h
   ast/const.cc
   ast/const.h
+  ast/const_assert.cc
+  ast/const_assert.h
   ast/continue_statement.cc
   ast/continue_statement.h
   ast/depth_multisampled_texture.cc
   ast/depth_multisampled_texture.h
   ast/depth_texture.cc
   ast/depth_texture.h
+  ast/diagnostic_attribute.cc
+  ast/diagnostic_attribute.h
   ast/diagnostic_control.cc
   ast/diagnostic_control.h
   ast/disable_validation_attribute.cc
@@ -206,8 +210,6 @@
   ast/stage_attribute.h
   ast/statement.cc
   ast/statement.h
-  ast/static_assert.cc
-  ast/static_assert.h
   ast/storage_texture.cc
   ast/storage_texture.h
   ast/stride_attribute.cc
@@ -616,6 +618,8 @@
 
 if(${TINT_BUILD_WGSL_READER})
   list(APPEND TINT_LIB_SRCS
+    reader/wgsl/classify_template_args.cc
+    reader/wgsl/classify_template_args.h
     reader/wgsl/lexer.cc
     reader/wgsl/lexer.h
     reader/wgsl/parser.cc
@@ -829,9 +833,11 @@
     ast/case_selector_test.cc
     ast/case_statement_test.cc
     ast/compound_assignment_statement_test.cc
+    ast/const_assert_test.cc
     ast/continue_statement_test.cc
     ast/depth_multisampled_texture_test.cc
     ast/depth_texture_test.cc
+    ast/diagnostic_attribute_test.cc
     ast/diagnostic_control_test.cc
     ast/discard_statement_test.cc
     ast/enable_test.cc
@@ -854,7 +860,6 @@
     ast/loop_statement_test.cc
     ast/matrix_test.cc
     ast/member_accessor_expression_test.cc
-    ast/module_clone_test.cc
     ast/module_test.cc
     ast/multisampled_texture_test.cc
     ast/phony_expression_test.cc
@@ -863,7 +868,6 @@
     ast/sampled_texture_test.cc
     ast/sampler_test.cc
     ast/stage_attribute_test.cc
-    ast/static_assert_test.cc
     ast/storage_texture_test.cc
     ast/stride_attribute_test.cc
     ast/struct_member_align_attribute_test.cc
@@ -910,6 +914,7 @@
     resolver/call_validation_test.cc
     resolver/compound_assignment_validation_test.cc
     resolver/compound_statement_test.cc
+    resolver/const_assert_test.cc
     resolver/const_eval_binary_op_test.cc
     resolver/const_eval_bitcast_test.cc
     resolver/const_eval_builtin_test.cc
@@ -921,6 +926,7 @@
     resolver/const_eval_unary_op_test.cc
     resolver/control_block_validation_test.cc
     resolver/dependency_graph_test.cc
+    resolver/diagnostic_control_test.cc
     resolver/entry_point_validation_test.cc
     resolver/evaluation_stage_test.cc
     resolver/f16_extension_test.cc
@@ -941,7 +947,6 @@
     resolver/resolver_test_helper.h
     resolver/resolver_test.cc
     resolver/side_effects_test.cc
-    resolver/static_assert_test.cc
     resolver/root_identifier_test.cc
     resolver/address_space_layout_validation_test.cc
     resolver/address_space_validation_test.cc
@@ -956,6 +961,7 @@
     resolver/variable_validation_test.cc
     scope_stack_test.cc
     sem/builtin_test.cc
+    sem/diagnostic_severity_test.cc
     sem/expression_test.cc
     sem/struct_test.cc
     source_test.cc
@@ -1074,6 +1080,7 @@
 
   if(${TINT_BUILD_WGSL_READER})
     list(APPEND TINT_TEST_SRCS
+      reader/wgsl/classify_template_args_test.cc
       reader/wgsl/lexer_test.cc
       reader/wgsl/parser_test.cc
       reader/wgsl/parser_impl_additive_expression_test.cc
@@ -1091,6 +1098,9 @@
       reader/wgsl/parser_impl_continuing_stmt_test.cc
       reader/wgsl/parser_impl_core_lhs_expression_test.cc
       reader/wgsl/parser_impl_depth_texture_test.cc
+      reader/wgsl/parser_impl_diagnostic_attribute_test.cc
+      reader/wgsl/parser_impl_diagnostic_control_test.cc
+      reader/wgsl/parser_impl_diagnostic_directive_test.cc
       reader/wgsl/parser_impl_element_count_expression_test.cc
       reader/wgsl/parser_impl_enable_directive_test.cc
       reader/wgsl/parser_impl_error_msg_test.cc
@@ -1164,7 +1174,7 @@
       writer/spirv/builder_builtin_test.cc
       writer/spirv/builder_builtin_texture_test.cc
       writer/spirv/builder_call_test.cc
-      writer/spirv/builder_initializer_expression_test.cc
+      writer/spirv/builder_const_assert_test.cc
       writer/spirv/builder_discard_test.cc
       writer/spirv/builder_entry_point_test.cc
       writer/spirv/builder_format_conversion_test.cc
@@ -1174,10 +1184,10 @@
       writer/spirv/builder_global_variable_test.cc
       writer/spirv/builder_ident_expression_test.cc
       writer/spirv/builder_if_test.cc
+      writer/spirv/builder_initializer_expression_test.cc
       writer/spirv/builder_literal_test.cc
       writer/spirv/builder_loop_test.cc
       writer/spirv/builder_return_test.cc
-      writer/spirv/builder_static_assert_test.cc
       writer/spirv/builder_switch_test.cc
       writer/spirv/builder_test.cc
       writer/spirv/builder_type_test.cc
@@ -1204,19 +1214,20 @@
       writer/wgsl/generator_impl_call_test.cc
       writer/wgsl/generator_impl_case_test.cc
       writer/wgsl/generator_impl_cast_test.cc
-      writer/wgsl/generator_impl_initializer_test.cc
+      writer/wgsl/generator_impl_const_assert_test.cc
       writer/wgsl/generator_impl_continue_test.cc
+      writer/wgsl/generator_impl_diagnostic_test.cc
       writer/wgsl/generator_impl_discard_test.cc
       writer/wgsl/generator_impl_enable_test.cc
       writer/wgsl/generator_impl_function_test.cc
       writer/wgsl/generator_impl_global_decl_test.cc
       writer/wgsl/generator_impl_identifier_test.cc
       writer/wgsl/generator_impl_if_test.cc
+      writer/wgsl/generator_impl_initializer_test.cc
       writer/wgsl/generator_impl_loop_test.cc
       writer/wgsl/generator_impl_literal_test.cc
       writer/wgsl/generator_impl_member_accessor_test.cc
       writer/wgsl/generator_impl_return_test.cc
-      writer/wgsl/generator_impl_static_assert_test.cc
       writer/wgsl/generator_impl_switch_test.cc
       writer/wgsl/generator_impl_type_test.cc
       writer/wgsl/generator_impl_unary_op_test.cc
@@ -1228,6 +1239,7 @@
 
   if(${TINT_BUILD_WGSL_READER} AND ${TINT_BUILD_WGSL_WRITER})
     list(APPEND TINT_TEST_SRCS
+      ast/module_clone_test.cc
       transform/add_empty_entry_point_test.cc
       transform/add_block_attribute_test.cc
       transform/array_length_from_uniform_test.cc
@@ -1298,19 +1310,19 @@
       writer/msl/generator_impl_call_test.cc
       writer/msl/generator_impl_case_test.cc
       writer/msl/generator_impl_cast_test.cc
-      writer/msl/generator_impl_initializer_test.cc
+      writer/msl/generator_impl_const_assert_test.cc
       writer/msl/generator_impl_continue_test.cc
       writer/msl/generator_impl_discard_test.cc
       writer/msl/generator_impl_function_test.cc
       writer/msl/generator_impl_identifier_test.cc
       writer/msl/generator_impl_if_test.cc
+      writer/msl/generator_impl_initializer_test.cc
       writer/msl/generator_impl_import_test.cc
       writer/msl/generator_impl_loop_test.cc
       writer/msl/generator_impl_member_accessor_test.cc
       writer/msl/generator_impl_module_constant_test.cc
       writer/msl/generator_impl_return_test.cc
       writer/msl/generator_impl_sanitizer_test.cc
-      writer/msl/generator_impl_static_assert_test.cc
       writer/msl/generator_impl_switch_test.cc
       writer/msl/generator_impl_test.cc
       writer/msl/generator_impl_type_test.cc
@@ -1370,19 +1382,19 @@
       writer/hlsl/generator_impl_call_test.cc
       writer/hlsl/generator_impl_case_test.cc
       writer/hlsl/generator_impl_cast_test.cc
-      writer/hlsl/generator_impl_initializer_test.cc
+      writer/hlsl/generator_impl_const_assert_test.cc
       writer/hlsl/generator_impl_continue_test.cc
       writer/hlsl/generator_impl_discard_test.cc
       writer/hlsl/generator_impl_function_test.cc
       writer/hlsl/generator_impl_identifier_test.cc
       writer/hlsl/generator_impl_if_test.cc
+      writer/hlsl/generator_impl_initializer_test.cc
       writer/hlsl/generator_impl_import_test.cc
       writer/hlsl/generator_impl_loop_test.cc
       writer/hlsl/generator_impl_member_accessor_test.cc
       writer/hlsl/generator_impl_module_constant_test.cc
       writer/hlsl/generator_impl_return_test.cc
       writer/hlsl/generator_impl_sanitizer_test.cc
-      writer/hlsl/generator_impl_static_assert_test.cc
       writer/hlsl/generator_impl_switch_test.cc
       writer/hlsl/generator_impl_test.cc
       writer/hlsl/generator_impl_type_test.cc
diff --git a/src/tint/ast/static_assert.cc b/src/tint/ast/const_assert.cc
similarity index 71%
rename from src/tint/ast/static_assert.cc
rename to src/tint/ast/const_assert.cc
index 0609194..5f9978d 100644
--- a/src/tint/ast/static_assert.cc
+++ b/src/tint/ast/const_assert.cc
@@ -12,29 +12,29 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/ast/static_assert.h"
+#include "src/tint/ast/const_assert.h"
 
 #include "src/tint/program_builder.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::StaticAssert);
+TINT_INSTANTIATE_TYPEINFO(tint::ast::ConstAssert);
 
 namespace tint::ast {
 
-StaticAssert::StaticAssert(ProgramID pid, NodeID nid, const Source& src, const Expression* cond)
+ConstAssert::ConstAssert(ProgramID pid, NodeID nid, const Source& src, const Expression* cond)
     : Base(pid, nid, src), condition(cond) {
     TINT_ASSERT(AST, cond);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, cond, program_id);
 }
 
-StaticAssert::StaticAssert(StaticAssert&&) = default;
+ConstAssert::ConstAssert(ConstAssert&&) = default;
 
-StaticAssert::~StaticAssert() = default;
+ConstAssert::~ConstAssert() = default;
 
-const StaticAssert* StaticAssert::Clone(CloneContext* ctx) const {
+const ConstAssert* ConstAssert::Clone(CloneContext* ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
     auto src = ctx->Clone(source);
     auto* cond = ctx->Clone(condition);
-    return ctx->dst->create<StaticAssert>(src, cond);
+    return ctx->dst->create<ConstAssert>(src, cond);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/ast/static_assert.h b/src/tint/ast/const_assert.h
similarity index 73%
rename from src/tint/ast/static_assert.h
rename to src/tint/ast/const_assert.h
index f42ad07..8f0aae5 100644
--- a/src/tint/ast/static_assert.h
+++ b/src/tint/ast/const_assert.h
@@ -12,34 +12,34 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_AST_STATIC_ASSERT_H_
-#define SRC_TINT_AST_STATIC_ASSERT_H_
+#ifndef SRC_TINT_AST_CONST_ASSERT_H_
+#define SRC_TINT_AST_CONST_ASSERT_H_
 
 #include "src/tint/ast/statement.h"
 #include "src/tint/ast/variable.h"
 
 namespace tint::ast {
 
-/// A `static_assert` statement
-class StaticAssert final : public Castable<StaticAssert, Statement> {
+/// A `const_assert` statement
+class ConstAssert final : public Castable<ConstAssert, Statement> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
     /// @param nid the unique node identifier
     /// @param source the variable statement source
     /// @param condition the assertion condition
-    StaticAssert(ProgramID pid, NodeID nid, const Source& source, const Expression* condition);
+    ConstAssert(ProgramID pid, NodeID nid, const Source& source, const Expression* condition);
 
     /// Move constructor
-    StaticAssert(StaticAssert&&);
+    ConstAssert(ConstAssert&&);
 
     /// Destructor
-    ~StaticAssert() override;
+    ~ConstAssert() override;
 
     /// Clones this node and all transitive child nodes using the `CloneContext` `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const StaticAssert* Clone(CloneContext* ctx) const override;
+    const ConstAssert* Clone(CloneContext* ctx) const override;
 
     /// The assertion condition
     const Expression* const condition;
@@ -47,4 +47,4 @@
 
 }  // namespace tint::ast
 
-#endif  // SRC_TINT_AST_STATIC_ASSERT_H_
+#endif  // SRC_TINT_AST_CONST_ASSERT_H_
diff --git a/src/tint/ast/static_assert_test.cc b/src/tint/ast/const_assert_test.cc
similarity index 68%
rename from src/tint/ast/static_assert_test.cc
rename to src/tint/ast/const_assert_test.cc
index 48bee48..ec938b7 100644
--- a/src/tint/ast/static_assert_test.cc
+++ b/src/tint/ast/const_assert_test.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/ast/static_assert.h"
+#include "src/tint/ast/const_assert.h"
 
 #include "gtest/gtest-spi.h"
 #include "src/tint/ast/test_helper.h"
@@ -20,44 +20,44 @@
 namespace tint::ast {
 namespace {
 
-using StaticAssertTest = TestHelper;
+using ConstAssertTest = TestHelper;
 
-TEST_F(StaticAssertTest, Creation) {
+TEST_F(ConstAssertTest, Creation) {
     auto* cond = Expr(true);
-    auto* stmt = StaticAssert(cond);
+    auto* stmt = ConstAssert(cond);
     EXPECT_EQ(stmt->condition, cond);
 }
 
-TEST_F(StaticAssertTest, Creation_WithSource) {
+TEST_F(ConstAssertTest, Creation_WithSource) {
     auto* cond = Expr(true);
-    auto* stmt = StaticAssert(Source{{20, 2}}, cond);
+    auto* stmt = ConstAssert(Source{{20, 2}}, cond);
     auto src = stmt->source;
     EXPECT_EQ(src.range.begin.line, 20u);
     EXPECT_EQ(src.range.begin.column, 2u);
 }
 
-TEST_F(StaticAssertTest, IsStaticAssert) {
+TEST_F(ConstAssertTest, IsConstAssert) {
     auto* cond = Expr(true);
 
-    auto* stmt = StaticAssert(cond);
-    EXPECT_TRUE(stmt->Is<ast::StaticAssert>());
+    auto* stmt = ConstAssert(cond);
+    EXPECT_TRUE(stmt->Is<ast::ConstAssert>());
 }
 
-TEST_F(StaticAssertTest, Assert_Null_Condition) {
+TEST_F(ConstAssertTest, Assert_Null_Condition) {
     EXPECT_FATAL_FAILURE(
         {
             ProgramBuilder b;
-            b.StaticAssert(nullptr);
+            b.ConstAssert(nullptr);
         },
         "internal compiler error");
 }
 
-TEST_F(StaticAssertTest, Assert_DifferentProgramID_Condition) {
+TEST_F(ConstAssertTest, Assert_DifferentProgramID_Condition) {
     EXPECT_FATAL_FAILURE(
         {
             ProgramBuilder b1;
             ProgramBuilder b2;
-            b1.StaticAssert(b2.Expr(i32(123)));
+            b1.ConstAssert(b2.Expr(i32(123)));
         },
         "internal compiler error");
 }
diff --git a/src/tint/ast/diagnostic_attribute.cc b/src/tint/ast/diagnostic_attribute.cc
new file mode 100644
index 0000000..21f698e
--- /dev/null
+++ b/src/tint/ast/diagnostic_attribute.cc
@@ -0,0 +1,44 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/diagnostic_attribute.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::DiagnosticAttribute);
+
+namespace tint::ast {
+
+DiagnosticAttribute::DiagnosticAttribute(ProgramID pid,
+                                         NodeID nid,
+                                         const Source& src,
+                                         const ast::DiagnosticControl* dc)
+    : Base(pid, nid, src), control(dc) {}
+
+DiagnosticAttribute::~DiagnosticAttribute() = default;
+
+std::string DiagnosticAttribute::Name() const {
+    return "diagnostic";
+}
+
+const DiagnosticAttribute* DiagnosticAttribute::Clone(CloneContext* ctx) const {
+    // Clone arguments outside of create() call to have deterministic ordering
+    auto src = ctx->Clone(source);
+    auto dc = ctx->Clone(control);
+    return ctx->dst->create<DiagnosticAttribute>(src, dc);
+}
+
+}  // namespace tint::ast
diff --git a/src/tint/ast/diagnostic_attribute.h b/src/tint/ast/diagnostic_attribute.h
new file mode 100644
index 0000000..7e0c514
--- /dev/null
+++ b/src/tint/ast/diagnostic_attribute.h
@@ -0,0 +1,53 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_DIAGNOSTIC_ATTRIBUTE_H_
+#define SRC_TINT_AST_DIAGNOSTIC_ATTRIBUTE_H_
+
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+#include "src/tint/ast/diagnostic_control.h"
+
+namespace tint::ast {
+
+/// A diagnostic attribute
+class DiagnosticAttribute final : public Castable<DiagnosticAttribute, Attribute> {
+  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 dc the diagnostic control
+    DiagnosticAttribute(ProgramID pid,
+                        NodeID nid,
+                        const Source& src,
+                        const ast::DiagnosticControl* dc);
+    ~DiagnosticAttribute() override;
+
+    /// @returns the WGSL name for the attribute
+    std::string Name() const override;
+
+    /// Clones this node and all transitive child nodes using the `CloneContext` `ctx`.
+    /// @param ctx the clone context
+    /// @return the newly cloned node
+    const DiagnosticAttribute* Clone(CloneContext* ctx) const override;
+
+    /// The diagnostic control expression.
+    const ast::DiagnosticControl* const control;
+};
+
+}  // namespace tint::ast
+
+#endif  // SRC_TINT_AST_DIAGNOSTIC_ATTRIBUTE_H_
diff --git a/src/tint/ast/diagnostic_attribute_test.cc b/src/tint/ast/diagnostic_attribute_test.cc
new file mode 100644
index 0000000..d7f0779
--- /dev/null
+++ b/src/tint/ast/diagnostic_attribute_test.cc
@@ -0,0 +1,32 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint::ast {
+namespace {
+
+using namespace tint::number_suffixes;  // NOLINT
+using DiagnosticAttributeTest = TestHelper;
+
+TEST_F(DiagnosticAttributeTest, Creation) {
+    auto* name = Expr("foo");
+    auto* d = DiagnosticAttribute(DiagnosticSeverity::kWarning, name);
+    EXPECT_EQ(d->Name(), "diagnostic");
+    EXPECT_EQ(d->control->severity, DiagnosticSeverity::kWarning);
+    EXPECT_EQ(d->control->rule_name, name);
+}
+
+}  // namespace
+}  // namespace tint::ast
diff --git a/src/tint/ast/diagnostic_control.cc b/src/tint/ast/diagnostic_control.cc
index d83e9c2..a814d58 100644
--- a/src/tint/ast/diagnostic_control.cc
+++ b/src/tint/ast/diagnostic_control.cc
@@ -38,6 +38,19 @@
     return ctx->dst->create<DiagnosticControl>(src, severity, rule);
 }
 
+diag::Severity ToSeverity(DiagnosticSeverity sc) {
+    switch (sc) {
+        case DiagnosticSeverity::kError:
+            return diag::Severity::Error;
+        case DiagnosticSeverity::kWarning:
+            return diag::Severity::Warning;
+        case DiagnosticSeverity::kInfo:
+            return diag::Severity::Note;
+        default:
+            return diag::Severity::InternalCompilerError;
+    }
+}
+
 /// ParseDiagnosticSeverity parses a DiagnosticSeverity from a string.
 /// @param str the string to parse
 /// @returns the parsed enum, or DiagnosticSeverity::kUndefined if the string could not be parsed.
@@ -73,4 +86,29 @@
     return out << "<unknown>";
 }
 
+/// ParseDiagnosticRule parses a DiagnosticRule from a string.
+/// @param str the string to parse
+/// @returns the parsed enum, or DiagnosticRule::kUndefined if the string could not be parsed.
+DiagnosticRule ParseDiagnosticRule(std::string_view str) {
+    if (str == "chromium_unreachable_code") {
+        return DiagnosticRule::kChromiumUnreachableCode;
+    }
+    if (str == "derivative_uniformity") {
+        return DiagnosticRule::kDerivativeUniformity;
+    }
+    return DiagnosticRule::kUndefined;
+}
+
+std::ostream& operator<<(std::ostream& out, DiagnosticRule value) {
+    switch (value) {
+        case DiagnosticRule::kUndefined:
+            return out << "undefined";
+        case DiagnosticRule::kChromiumUnreachableCode:
+            return out << "chromium_unreachable_code";
+        case DiagnosticRule::kDerivativeUniformity:
+            return out << "derivative_uniformity";
+    }
+    return out << "<unknown>";
+}
+
 }  // namespace tint::ast
diff --git a/src/tint/ast/diagnostic_control.cc.tmpl b/src/tint/ast/diagnostic_control.cc.tmpl
index c5d1a73..c6dc463 100644
--- a/src/tint/ast/diagnostic_control.cc.tmpl
+++ b/src/tint/ast/diagnostic_control.cc.tmpl
@@ -28,8 +28,25 @@
     return ctx->dst->create<DiagnosticControl>(src, severity, rule);
 }
 
+diag::Severity ToSeverity(DiagnosticSeverity sc) {
+    switch (sc) {
+        case DiagnosticSeverity::kError:
+            return diag::Severity::Error;
+        case DiagnosticSeverity::kWarning:
+            return diag::Severity::Warning;
+        case DiagnosticSeverity::kInfo:
+            return diag::Severity::Note;
+        default:
+            return diag::Severity::InternalCompilerError;
+    }
+}
+
 {{ Eval "ParseEnum" (Sem.Enum "diagnostic_severity")}}
 
 {{ Eval "EnumOStream" (Sem.Enum "diagnostic_severity")}}
 
+{{ Eval "ParseEnum" (Sem.Enum "diagnostic_rule")}}
+
+{{ Eval "EnumOStream" (Sem.Enum "diagnostic_rule")}}
+
 }  // namespace tint::ast
diff --git a/src/tint/ast/diagnostic_control.h b/src/tint/ast/diagnostic_control.h
index f127c1f..7b6e28c 100644
--- a/src/tint/ast/diagnostic_control.h
+++ b/src/tint/ast/diagnostic_control.h
@@ -25,6 +25,7 @@
 
 #include <ostream>
 #include <string>
+#include <unordered_map>
 
 #include "src/tint/ast/node.h"
 
@@ -61,6 +62,34 @@
     "warning",
 };
 
+/// The diagnostic rule.
+enum class DiagnosticRule {
+    kUndefined,
+    kChromiumUnreachableCode,
+    kDerivativeUniformity,
+};
+
+/// @param out the std::ostream to write to
+/// @param value the DiagnosticRule
+/// @returns `out` so calls can be chained
+std::ostream& operator<<(std::ostream& out, DiagnosticRule value);
+
+/// ParseDiagnosticRule parses a DiagnosticRule from a string.
+/// @param str the string to parse
+/// @returns the parsed enum, or DiagnosticRule::kUndefined if the string could not be parsed.
+DiagnosticRule ParseDiagnosticRule(std::string_view str);
+
+constexpr const char* kDiagnosticRuleStrings[] = {
+    "chromium_unreachable_code",
+    "derivative_uniformity",
+};
+
+/// Convert a DiagnosticSeverity to the corresponding diag::Severity.
+diag::Severity ToSeverity(DiagnosticSeverity sc);
+
+/// DiagnosticRuleSeverities is a map from diagnostic rule to diagnostic severity.
+using DiagnosticRuleSeverities = std::unordered_map<DiagnosticRule, DiagnosticSeverity>;
+
 /// A diagnostic control used for diagnostic directives and attributes.
 class DiagnosticControl : public Castable<DiagnosticControl, Node> {
   public:
diff --git a/src/tint/ast/diagnostic_control.h.tmpl b/src/tint/ast/diagnostic_control.h.tmpl
index 5ddbc6d..49946c2 100644
--- a/src/tint/ast/diagnostic_control.h.tmpl
+++ b/src/tint/ast/diagnostic_control.h.tmpl
@@ -15,6 +15,7 @@
 
 #include <ostream>
 #include <string>
+#include <unordered_map>
 
 #include "src/tint/ast/node.h"
 
@@ -28,6 +29,15 @@
 /// The diagnostic severity control.
 {{ Eval "DeclareEnum" (Sem.Enum "diagnostic_severity") }}
 
+/// The diagnostic rule.
+{{ Eval "DeclareEnum" (Sem.Enum "diagnostic_rule") }}
+
+/// Convert a DiagnosticSeverity to the corresponding diag::Severity.
+diag::Severity ToSeverity(DiagnosticSeverity sc);
+
+/// DiagnosticRuleSeverities is a map from diagnostic rule to diagnostic severity.
+using DiagnosticRuleSeverities = std::unordered_map<DiagnosticRule, DiagnosticSeverity>;
+
 /// A diagnostic control used for diagnostic directives and attributes.
 class DiagnosticControl : public Castable<DiagnosticControl, Node> {
   public:
diff --git a/src/tint/ast/diagnostic_control_bench.cc b/src/tint/ast/diagnostic_control_bench.cc
index d96f93b..faf354e 100644
--- a/src/tint/ast/diagnostic_control_bench.cc
+++ b/src/tint/ast/diagnostic_control_bench.cc
@@ -46,5 +46,23 @@
 
 BENCHMARK(DiagnosticSeverityParser);
 
+void DiagnosticRuleParser(::benchmark::State& state) {
+    std::array kStrings{
+        "hromium_unyeachable_code",   "chrorrillmGunnreachable_c77de", "chromium_unreachable4cod00",
+        "chromium_unreachable_code",  "chromium_unracaboo_code",       "chromium_unrzzchabl_code",
+        "ciipp11ium_unreachable_cod", "derivXXtive_uniformity",        "55erivativeIIunifonn99ity",
+        "derirratHHaae_YniforSSity",  "derivative_uniformity",         "erivtive_unHkkormit",
+        "jerivaive_uniforRgty",       "derivatbve_unformiy",
+    };
+    for (auto _ : state) {
+        for (auto& str : kStrings) {
+            auto result = ParseDiagnosticRule(str);
+            benchmark::DoNotOptimize(result);
+        }
+    }
+}
+
+BENCHMARK(DiagnosticRuleParser);
+
 }  // namespace
 }  // namespace tint::ast
diff --git a/src/tint/ast/diagnostic_control_bench.cc.tmpl b/src/tint/ast/diagnostic_control_bench.cc.tmpl
index 48058ca..55d3cce 100644
--- a/src/tint/ast/diagnostic_control_bench.cc.tmpl
+++ b/src/tint/ast/diagnostic_control_bench.cc.tmpl
@@ -21,5 +21,7 @@
 
 {{ Eval "BenchmarkParseEnum" (Sem.Enum "diagnostic_severity")}}
 
+{{ Eval "BenchmarkParseEnum" (Sem.Enum "diagnostic_rule")}}
+
 }  // namespace
 }  // namespace tint::ast
diff --git a/src/tint/ast/diagnostic_control_test.cc b/src/tint/ast/diagnostic_control_test.cc
index 2dbd139..11a544a 100644
--- a/src/tint/ast/diagnostic_control_test.cc
+++ b/src/tint/ast/diagnostic_control_test.cc
@@ -100,5 +100,57 @@
 
 }  // namespace diagnostic_severity_tests
 
+namespace diagnostic_rule_tests {
+
+namespace parse_print_tests {
+
+struct Case {
+    const char* string;
+    DiagnosticRule value;
+};
+
+inline std::ostream& operator<<(std::ostream& out, Case c) {
+    return out << "'" << std::string(c.string) << "'";
+}
+
+static constexpr Case kValidCases[] = {
+    {"chromium_unreachable_code", DiagnosticRule::kChromiumUnreachableCode},
+    {"derivative_uniformity", DiagnosticRule::kDerivativeUniformity},
+};
+
+static constexpr Case kInvalidCases[] = {
+    {"cXromggum_unreachable_cde", DiagnosticRule::kUndefined},
+    {"chroVium_unruchble_codX", DiagnosticRule::kUndefined},
+    {"chromium_3nreachable_code", DiagnosticRule::kUndefined},
+    {"derivatEve_uniformity", DiagnosticRule::kUndefined},
+    {"deTTPivative_uniformit", DiagnosticRule::kUndefined},
+    {"derivtive_uddxxformity", DiagnosticRule::kUndefined},
+};
+
+using DiagnosticRuleParseTest = testing::TestWithParam<Case>;
+
+TEST_P(DiagnosticRuleParseTest, Parse) {
+    const char* string = GetParam().string;
+    DiagnosticRule expect = GetParam().value;
+    EXPECT_EQ(expect, ParseDiagnosticRule(string));
+}
+
+INSTANTIATE_TEST_SUITE_P(ValidCases, DiagnosticRuleParseTest, testing::ValuesIn(kValidCases));
+INSTANTIATE_TEST_SUITE_P(InvalidCases, DiagnosticRuleParseTest, testing::ValuesIn(kInvalidCases));
+
+using DiagnosticRulePrintTest = testing::TestWithParam<Case>;
+
+TEST_P(DiagnosticRulePrintTest, Print) {
+    DiagnosticRule value = GetParam().value;
+    const char* expect = GetParam().string;
+    EXPECT_EQ(expect, utils::ToString(value));
+}
+
+INSTANTIATE_TEST_SUITE_P(ValidCases, DiagnosticRulePrintTest, testing::ValuesIn(kValidCases));
+
+}  // namespace parse_print_tests
+
+}  // namespace diagnostic_rule_tests
+
 }  // namespace
 }  // namespace tint::ast
diff --git a/src/tint/ast/diagnostic_control_test.cc.tmpl b/src/tint/ast/diagnostic_control_test.cc.tmpl
index a078cce..2d1a2cc 100644
--- a/src/tint/ast/diagnostic_control_test.cc.tmpl
+++ b/src/tint/ast/diagnostic_control_test.cc.tmpl
@@ -41,5 +41,11 @@
 
 }  // namespace diagnostic_severity_tests
 
+namespace diagnostic_rule_tests {
+
+{{ Eval "TestParsePrintEnum" (Sem.Enum "diagnostic_rule")}}
+
+}  // namespace diagnostic_rule_tests
+
 }  // namespace
 }  // namespace tint::ast
diff --git a/src/tint/ast/module.cc b/src/tint/ast/module.cc
index 5003996..843f1fc 100644
--- a/src/tint/ast/module.cc
+++ b/src/tint/ast/module.cc
@@ -71,17 +71,28 @@
             TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, var, program_id);
             global_variables_.Push(var);
         },
+        [&](const DiagnosticControl* diag_control) {
+            TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, diag_control, program_id);
+            diagnostic_controls_.Push(diag_control);
+        },
         [&](const Enable* enable) {
             TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, enable, program_id);
             enables_.Push(enable);
         },
-        [&](const StaticAssert* assertion) {
+        [&](const ConstAssert* assertion) {
             TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, assertion, program_id);
-            static_asserts_.Push(assertion);
+            const_asserts_.Push(assertion);
         },
         [&](Default) { TINT_ICE(AST, diags) << "Unknown global declaration type"; });
 }
 
+void Module::AddDiagnosticControl(const ast::DiagnosticControl* control) {
+    TINT_ASSERT(AST, control);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, control, program_id);
+    global_declarations_.Push(control);
+    diagnostic_controls_.Push(control);
+}
+
 void Module::AddEnable(const ast::Enable* enable) {
     TINT_ASSERT(AST, enable);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, enable, program_id);
@@ -96,10 +107,10 @@
     global_declarations_.Push(var);
 }
 
-void Module::AddStaticAssert(const StaticAssert* assertion) {
+void Module::AddConstAssert(const ConstAssert* assertion) {
     TINT_ASSERT(AST, assertion);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, assertion, program_id);
-    static_asserts_.Push(assertion);
+    const_asserts_.Push(assertion);
     global_declarations_.Push(assertion);
 }
 
diff --git a/src/tint/ast/module.h b/src/tint/ast/module.h
index 2818cb8..7d0cdfd 100644
--- a/src/tint/ast/module.h
+++ b/src/tint/ast/module.h
@@ -17,9 +17,10 @@
 
 #include <string>
 
+#include "src/tint/ast/const_assert.h"
+#include "src/tint/ast/diagnostic_control.h"
 #include "src/tint/ast/enable.h"
 #include "src/tint/ast/function.h"
-#include "src/tint/ast/static_assert.h"
 #include "src/tint/ast/type.h"
 #include "src/tint/utils/vector.h"
 
@@ -92,19 +93,26 @@
         return out;
     }
 
+    /// Add a global diagnostic control to the module
+    /// @param control the diagnostic control to add
+    void AddDiagnosticControl(const DiagnosticControl* control);
+
     /// Add a enable directive to the module
     /// @param ext the enable directive to add
     void AddEnable(const Enable* ext);
 
+    /// @returns the global diagnostic controls for the module
+    const auto& DiagnosticControls() const { return diagnostic_controls_; }
+
     /// @returns the extension set for the module
     const auto& Enables() const { return enables_; }
 
-    /// Add a global static assertion to the module
-    /// @param assertion the static assert to add
-    void AddStaticAssert(const StaticAssert* assertion);
+    /// Add a global const assertion to the module
+    /// @param assertion the const assert to add
+    void AddConstAssert(const ConstAssert* assertion);
 
-    /// @returns the list of global static assertions
-    const auto& StaticAsserts() const { return static_asserts_; }
+    /// @returns the list of global const assertions
+    const auto& ConstAsserts() const { return const_asserts_; }
 
     /// Adds a type declaration to the module
     /// @param decl the type declaration to add
@@ -146,8 +154,9 @@
     utils::Vector<const TypeDecl*, 16> type_decls_;
     FunctionList functions_;
     utils::Vector<const Variable*, 32> global_variables_;
+    utils::Vector<const DiagnosticControl*, 8> diagnostic_controls_;
     utils::Vector<const Enable*, 8> enables_;
-    utils::Vector<const StaticAssert*, 8> static_asserts_;
+    utils::Vector<const ConstAssert*, 8> const_asserts_;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/ast/module_clone_test.cc b/src/tint/ast/module_clone_test.cc
index 09dced5..7bda881 100644
--- a/src/tint/ast/module_clone_test.cc
+++ b/src/tint/ast/module_clone_test.cc
@@ -22,10 +22,12 @@
 namespace {
 
 TEST(ModuleCloneTest, Clone) {
-#if TINT_BUILD_WGSL_READER && TINT_BUILD_WGSL_WRITER
     // Shader that exercises the bulk of the AST nodes and types.
     // See also fuzzers/tint_ast_clone_fuzzer.cc for further coverage of cloning.
-    Source::File file("test.wgsl", R"(struct S0 {
+    Source::File file("test.wgsl", R"(enable f16;
+diagnostic(off, chromium_unreachable_code);
+
+struct S0 {
   @size(4)
   m0 : u32,
   m1 : array<u32>,
@@ -63,6 +65,7 @@
   return 0.0;
 }
 
+@diagnostic(warning, chromium_unreachable_code)
 fn f1(p0 : f32, p1 : i32) -> f32 {
   var l0 : i32 = 3;
   var l1 : f32 = 8.0;
@@ -170,11 +173,6 @@
     ASSERT_TRUE(result.success);
     auto dst_wgsl = result.wgsl;
     ASSERT_EQ(src_wgsl, dst_wgsl);
-
-#else  // #if TINT_BUILD_WGSL_READER && TINT_BUILD_WGSL_WRITER
-    GTEST_SKIP() << "ModuleCloneTest requires TINT_BUILD_WGSL_READER and "
-                    "TINT_BUILD_WGSL_WRITER to be enabled";
-#endif
 }
 
 }  // namespace
diff --git a/src/tint/ast/module_test.cc b/src/tint/ast/module_test.cc
index c7b77c9..980645a 100644
--- a/src/tint/ast/module_test.cc
+++ b/src/tint/ast/module_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "gmock/gmock.h"
 #include "gtest/gtest-spi.h"
 #include "src/tint/ast/test_helper.h"
 #include "src/tint/clone_context.h"
@@ -130,5 +131,29 @@
     ASSERT_EQ(cloned.Symbols().NameFor(decls[4]->As<ast::Alias>()->name), "inserted_before_V");
 }
 
+TEST_F(ModuleTest, Directives) {
+    auto* enable_1 = Enable(ast::Extension::kF16);
+    auto* diagnostic_1 = DiagnosticDirective(DiagnosticSeverity::kWarning, Expr("foo"));
+    auto* enable_2 = Enable(ast::Extension::kChromiumExperimentalFullPtrParameters);
+    auto* diagnostic_2 = DiagnosticDirective(DiagnosticSeverity::kOff, Expr("bar"));
+
+    this->SetResolveOnBuild(false);
+    Program program(std::move(*this));
+    EXPECT_THAT(program.AST().GlobalDeclarations(), ::testing::ContainerEq(utils::Vector{
+                                                        enable_1,
+                                                        diagnostic_1,
+                                                        enable_2,
+                                                        diagnostic_2,
+                                                    }));
+    EXPECT_THAT(program.AST().Enables(), ::testing::ContainerEq(utils::Vector{
+                                             enable_1,
+                                             enable_2,
+                                         }));
+    EXPECT_THAT(program.AST().DiagnosticControls(), ::testing::ContainerEq(utils::Vector{
+                                                        diagnostic_1,
+                                                        diagnostic_2,
+                                                    }));
+}
+
 }  // namespace
 }  // namespace tint::ast
diff --git a/src/tint/cmd/main.cc b/src/tint/cmd/main.cc
index a30a133..fa8dbc0 100644
--- a/src/tint/cmd/main.cc
+++ b/src/tint/cmd/main.cc
@@ -103,6 +103,10 @@
 
     bool rename_all = false;
 
+#if TINT_BUILD_SPV_READER
+    tint::reader::spirv::Options spirv_reader_options;
+#endif
+
     std::vector<std::string> transforms;
 
     std::string fxc_path;
@@ -135,6 +139,9 @@
   --transform <name list>   -- Runs transforms, name list is comma separated
                                Available transforms:
 ${transforms} --parse-only              -- Stop after parsing the input
+  --allow-non-uniform-derivatives  -- When using SPIR-V input, allow non-uniform derivatives by
+                               inserting a module-scope directive to suppress any uniformity
+                               violations that may be produced.
   --disable-workgroup-init  -- Disable workgroup memory zero initialization.
   --demangle                -- Preserve original source names. Demangle them.
                                Affects AST dumping, and text-based output languages.
@@ -443,6 +450,13 @@
             opts->transforms = split_on_comma(args[i]);
         } else if (arg == "--parse-only") {
             opts->parse_only = true;
+        } else if (arg == "--allow-non-uniform-derivatives") {
+#if TINT_BUILD_SPV_READER
+            opts->spirv_reader_options.allow_non_uniform_derivatives = true;
+#else
+            std::cerr << "Tint not built with the SPIR-V reader enabled" << std::endl;
+            return false;
+#endif
         } else if (arg == "--disable-workgroup-init") {
             opts->disable_workgroup_init = true;
         } else if (arg == "--demangle") {
@@ -1285,7 +1299,8 @@
             if (!ReadFile<uint32_t>(options.input_filename, &data)) {
                 return 1;
             }
-            program = std::make_unique<tint::Program>(tint::reader::spirv::Parse(data));
+            program = std::make_unique<tint::Program>(
+                tint::reader::spirv::Parse(data, options.spirv_reader_options));
             break;
 #else
             std::cerr << "Tint not built with the SPIR-V reader enabled" << std::endl;
@@ -1309,7 +1324,8 @@
                                 SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS)) {
                 return 1;
             }
-            program = std::make_unique<tint::Program>(tint::reader::spirv::Parse(data));
+            program = std::make_unique<tint::Program>(
+                tint::reader::spirv::Parse(data, options.spirv_reader_options));
             break;
 #else
             std::cerr << "Tint not built with the SPIR-V reader enabled" << std::endl;
diff --git a/src/tint/fuzzers/data_builder.h b/src/tint/fuzzers/data_builder.h
index 2aaecf7..d9e8e18 100644
--- a/src/tint/fuzzers/data_builder.h
+++ b/src/tint/fuzzers/data_builder.h
@@ -23,8 +23,6 @@
 #include <vector>
 
 #include "src/tint/fuzzers/random_generator.h"
-#include "src/tint/writer/hlsl/generator.h"
-#include "src/tint/writer/msl/generator.h"
 
 namespace tint::fuzzers {
 
diff --git a/src/tint/intrinsics.def b/src/tint/intrinsics.def
index 05fe8c9..f3eb2ac 100644
--- a/src/tint/intrinsics.def
+++ b/src/tint/intrinsics.def
@@ -40,6 +40,14 @@
   @internal point_size
 }
 
+// https://gpuweb.github.io/gpuweb/wgsl/#filterable-triggering-rules
+enum diagnostic_rule {
+  // Rules defined in the spec.
+  derivative_uniformity
+  // Chromium specific rules not defined in the spec.
+  chromium_unreachable_code
+}
+
 // https://gpuweb.github.io/gpuweb/wgsl/#syntax-severity_control_name
 enum diagnostic_severity {
   error
diff --git a/src/tint/ir/binary_test.cc b/src/tint/ir/binary_test.cc
index 68341ad..b5fba93 100644
--- a/src/tint/ir/binary_test.cc
+++ b/src/tint/ir/binary_test.cc
@@ -521,15 +521,15 @@
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kAnd);
 
     ASSERT_NE(instr->Result(), nullptr);
-    ASSERT_EQ(instr->Result()->Usage().Length(), 1);
+    ASSERT_EQ(instr->Result()->Usage().Length(), 1u);
     EXPECT_EQ(instr->Result()->Usage()[0], instr);
 
     ASSERT_NE(instr->LHS(), nullptr);
-    ASSERT_EQ(instr->LHS()->Usage().Length(), 1);
+    ASSERT_EQ(instr->LHS()->Usage().Length(), 1u);
     EXPECT_EQ(instr->LHS()->Usage()[0], instr);
 
     ASSERT_NE(instr->RHS(), nullptr);
-    ASSERT_EQ(instr->RHS()->Usage().Length(), 1);
+    ASSERT_EQ(instr->RHS()->Usage().Length(), 1u);
     EXPECT_EQ(instr->RHS()->Usage()[0], instr);
 }
 
@@ -544,13 +544,13 @@
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kAnd);
 
     ASSERT_NE(instr->Result(), nullptr);
-    ASSERT_EQ(instr->Result()->Usage().Length(), 1);
+    ASSERT_EQ(instr->Result()->Usage().Length(), 1u);
     EXPECT_EQ(instr->Result()->Usage()[0], instr);
 
     ASSERT_EQ(instr->LHS(), instr->RHS());
 
     ASSERT_NE(instr->LHS(), nullptr);
-    ASSERT_EQ(instr->LHS()->Usage().Length(), 1);
+    ASSERT_EQ(instr->LHS()->Usage().Length(), 1u);
     EXPECT_EQ(instr->LHS()->Usage()[0], instr);
 }
 
diff --git a/src/tint/ir/bitcast_test.cc b/src/tint/ir/bitcast_test.cc
index a6924df..ee7bd3d 100644
--- a/src/tint/ir/bitcast_test.cc
+++ b/src/tint/ir/bitcast_test.cc
@@ -53,11 +53,11 @@
         b.builder.Bitcast(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
 
     ASSERT_NE(instr->Result(), nullptr);
-    ASSERT_EQ(instr->Result()->Usage().Length(), 1);
+    ASSERT_EQ(instr->Result()->Usage().Length(), 1u);
     EXPECT_EQ(instr->Result()->Usage()[0], instr);
 
     ASSERT_NE(instr->Val(), nullptr);
-    ASSERT_EQ(instr->Val()->Usage().Length(), 1);
+    ASSERT_EQ(instr->Val()->Usage().Length(), 1u);
     EXPECT_EQ(instr->Val()->Usage()[0], instr);
 }
 
diff --git a/src/tint/ir/builder_impl.cc b/src/tint/ir/builder_impl.cc
index 0bcaf24..2a8ed06 100644
--- a/src/tint/ir/builder_impl.cc
+++ b/src/tint/ir/builder_impl.cc
@@ -21,6 +21,7 @@
 #include "src/tint/ast/bool_literal_expression.h"
 #include "src/tint/ast/break_if_statement.h"
 #include "src/tint/ast/break_statement.h"
+#include "src/tint/ast/const_assert.h"
 #include "src/tint/ast/continue_statement.h"
 #include "src/tint/ast/float_literal_expression.h"
 #include "src/tint/ast/for_loop_statement.h"
@@ -33,7 +34,6 @@
 #include "src/tint/ast/override.h"
 #include "src/tint/ast/return_statement.h"
 #include "src/tint/ast/statement.h"
-#include "src/tint/ast/static_assert.h"
 #include "src/tint/ast/struct.h"
 #include "src/tint/ast/struct_member_align_attribute.h"
 #include "src/tint/ast/struct_member_size_attribute.h"
@@ -155,7 +155,7 @@
             // TODO(dsinclair): Implement? I think these need to be passed along so further stages
             // know what is enabled.
             // },
-            [&](const ast::StaticAssert*) {
+            [&](const ast::ConstAssert*) {
                 // Evaluated by the resolver, drop from the IR.
                 return true;
             },
@@ -253,7 +253,7 @@
         [&](const ast::ReturnStatement* r) { return EmitReturn(r); },
         [&](const ast::SwitchStatement* s) { return EmitSwitch(s); },
         [&](const ast::VariableDeclStatement* v) { return EmitVariable(v->variable); },
-        [&](const ast::StaticAssert*) {
+        [&](const ast::ConstAssert*) {
             return true;  // Not emitted
         },
         [&](Default) {
diff --git a/src/tint/program_builder.h b/src/tint/program_builder.h
index 4e760de..c05abf9 100644
--- a/src/tint/program_builder.h
+++ b/src/tint/program_builder.h
@@ -37,9 +37,11 @@
 #include "src/tint/ast/case_statement.h"
 #include "src/tint/ast/compound_assignment_statement.h"
 #include "src/tint/ast/const.h"
+#include "src/tint/ast/const_assert.h"
 #include "src/tint/ast/continue_statement.h"
 #include "src/tint/ast/depth_multisampled_texture.h"
 #include "src/tint/ast/depth_texture.h"
+#include "src/tint/ast/diagnostic_attribute.h"
 #include "src/tint/ast/diagnostic_control.h"
 #include "src/tint/ast/disable_validation_attribute.h"
 #include "src/tint/ast/discard_statement.h"
@@ -72,7 +74,6 @@
 #include "src/tint/ast/sampled_texture.h"
 #include "src/tint/ast/sampler.h"
 #include "src/tint/ast/stage_attribute.h"
-#include "src/tint/ast/static_assert.h"
 #include "src/tint/ast/storage_texture.h"
 #include "src/tint/ast/stride_attribute.h"
 #include "src/tint/ast/struct_member_align_attribute.h"
@@ -1926,38 +1927,38 @@
 
     /// @param source the source information
     /// @param condition the assertion condition
-    /// @returns a new `ast::StaticAssert`, which is automatically registered as a global statement
+    /// @returns a new `ast::ConstAssert`, which is automatically registered as a global statement
     /// with the ast::Module.
     template <typename EXPR>
-    const ast::StaticAssert* GlobalStaticAssert(const Source& source, EXPR&& condition) {
-        auto* sa = StaticAssert(source, std::forward<EXPR>(condition));
-        AST().AddStaticAssert(sa);
+    const ast::ConstAssert* GlobalConstAssert(const Source& source, EXPR&& condition) {
+        auto* sa = ConstAssert(source, std::forward<EXPR>(condition));
+        AST().AddConstAssert(sa);
         return sa;
     }
 
     /// @param condition the assertion condition
-    /// @returns a new `ast::StaticAssert`, which is automatically registered as a global statement
+    /// @returns a new `ast::ConstAssert`, which is automatically registered as a global statement
     /// with the ast::Module.
     template <typename EXPR, typename = DisableIfSource<EXPR>>
-    const ast::StaticAssert* GlobalStaticAssert(EXPR&& condition) {
-        auto* sa = StaticAssert(std::forward<EXPR>(condition));
-        AST().AddStaticAssert(sa);
+    const ast::ConstAssert* GlobalConstAssert(EXPR&& condition) {
+        auto* sa = ConstAssert(std::forward<EXPR>(condition));
+        AST().AddConstAssert(sa);
         return sa;
     }
 
     /// @param source the source information
     /// @param condition the assertion condition
-    /// @returns a new `ast::StaticAssert` with the given assertion condition
+    /// @returns a new `ast::ConstAssert` with the given assertion condition
     template <typename EXPR>
-    const ast::StaticAssert* StaticAssert(const Source& source, EXPR&& condition) {
-        return create<ast::StaticAssert>(source, Expr(std::forward<EXPR>(condition)));
+    const ast::ConstAssert* ConstAssert(const Source& source, EXPR&& condition) {
+        return create<ast::ConstAssert>(source, Expr(std::forward<EXPR>(condition)));
     }
 
     /// @param condition the assertion condition
-    /// @returns a new `ast::StaticAssert` with the given assertion condition
+    /// @returns a new `ast::ConstAssert` with the given assertion condition
     template <typename EXPR, typename = DisableIfSource<EXPR>>
-    const ast::StaticAssert* StaticAssert(EXPR&& condition) {
-        return create<ast::StaticAssert>(Expr(std::forward<EXPR>(condition)));
+    const ast::ConstAssert* ConstAssert(EXPR&& condition) {
+        return create<ast::ConstAssert>(Expr(std::forward<EXPR>(condition)));
     }
 
     /// @param source the source information
@@ -3221,6 +3222,30 @@
                                                                   validation);
     }
 
+    /// Creates an ast::DiagnosticAttribute
+    /// @param source the source information
+    /// @param severity the diagnostic severity control
+    /// @param rule_name the diagnostic rule name
+    /// @returns the diagnostic attribute pointer
+    const ast::DiagnosticAttribute* DiagnosticAttribute(
+        const Source& source,
+        ast::DiagnosticSeverity severity,
+        const ast::IdentifierExpression* rule_name) {
+        return create<ast::DiagnosticAttribute>(source,
+                                                DiagnosticControl(source, severity, rule_name));
+    }
+
+    /// Creates an ast::DiagnosticAttribute
+    /// @param severity the diagnostic severity control
+    /// @param rule_name the diagnostic rule name
+    /// @returns the diagnostic attribute pointer
+    const ast::DiagnosticAttribute* DiagnosticAttribute(
+        ast::DiagnosticSeverity severity,
+        const ast::IdentifierExpression* rule_name) {
+        return create<ast::DiagnosticAttribute>(source_,
+                                                DiagnosticControl(source_, severity, rule_name));
+    }
+
     /// Creates an ast::DiagnosticControl
     /// @param source the source information
     /// @param severity the diagnostic severity control
@@ -3241,6 +3266,30 @@
         return create<ast::DiagnosticControl>(source_, severity, rule_name);
     }
 
+    /// Add a global diagnostic control to the module.
+    /// @param source the source information
+    /// @param severity the diagnostic severity control
+    /// @param rule_name the diagnostic rule name
+    /// @returns the diagnostic control pointer
+    const ast::DiagnosticControl* DiagnosticDirective(const Source& source,
+                                                      ast::DiagnosticSeverity severity,
+                                                      const ast::IdentifierExpression* rule_name) {
+        auto* control = DiagnosticControl(source, severity, rule_name);
+        AST().AddDiagnosticControl(control);
+        return control;
+    }
+
+    /// Add a global diagnostic control to the module.
+    /// @param severity the diagnostic severity control
+    /// @param rule_name the diagnostic rule name
+    /// @returns the diagnostic control pointer
+    const ast::DiagnosticControl* DiagnosticDirective(ast::DiagnosticSeverity severity,
+                                                      const ast::IdentifierExpression* rule_name) {
+        auto* control = DiagnosticControl(source_, severity, rule_name);
+        AST().AddDiagnosticControl(control);
+        return control;
+    }
+
     /// Sets the current builder source to `src`
     /// @param src the Source used for future create() calls
     void SetSource(const Source& src) {
diff --git a/src/tint/reader/spirv/function_memory_test.cc b/src/tint/reader/spirv/function_memory_test.cc
index 0ff9ba5..1877725 100644
--- a/src/tint/reader/spirv/function_memory_test.cc
+++ b/src/tint/reader/spirv/function_memory_test.cc
@@ -950,7 +950,7 @@
     auto p = parser(test::Assemble(assembly));
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << assembly << p->error();
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr(R"(type RTArr = @stride(4) array<u32>;
+    EXPECT_THAT(module_str, HasSubstr(R"(alias RTArr = @stride(4) array<u32>;
 
 struct S {
   /* @offset(0) */
diff --git a/src/tint/reader/spirv/parser.cc b/src/tint/reader/spirv/parser.cc
index ac43b9e..41e6df3 100644
--- a/src/tint/reader/spirv/parser.cc
+++ b/src/tint/reader/spirv/parser.cc
@@ -27,7 +27,7 @@
 
 namespace tint::reader::spirv {
 
-Program Parse(const std::vector<uint32_t>& input) {
+Program Parse(const std::vector<uint32_t>& input, const Options& options) {
     ParserImpl parser(input);
     bool parsed = parser.Parse();
 
@@ -38,6 +38,13 @@
         return Program(std::move(builder));
     }
 
+    if (options.allow_non_uniform_derivatives) {
+        // Suppress errors regarding non-uniform derivative operations if requested, by adding a
+        // diagnostic directive to the module.
+        builder.DiagnosticDirective(ast::DiagnosticSeverity::kOff,
+                                    builder.Expr("derivative_uniformity"));
+    }
+
     // The SPIR-V parser can construct disjoint AST nodes, which is invalid for
     // the Resolver. Clone the Program to clean these up.
     builder.SetResolveOnBuild(false);
diff --git a/src/tint/reader/spirv/parser.h b/src/tint/reader/spirv/parser.h
index 3641e08..78c80c0 100644
--- a/src/tint/reader/spirv/parser.h
+++ b/src/tint/reader/spirv/parser.h
@@ -21,13 +21,20 @@
 
 namespace tint::reader::spirv {
 
+/// Options that control how the SPIR-V parser should behave.
+struct Options {
+    /// Set to `true` to allow calls to derivative builtins in non-uniform control flow.
+    bool allow_non_uniform_derivatives = false;
+};
+
 /// Parses the SPIR-V source data, returning the parsed program.
 /// If the source data fails to parse then the returned
 /// `program.Diagnostics.contains_errors()` will be true, and the
 /// `program.Diagnostics()` will describe the error.
 /// @param input the source data
+/// @param options the parser options
 /// @returns the parsed program
-Program Parse(const std::vector<uint32_t>& input);
+Program Parse(const std::vector<uint32_t>& input, const Options& options = {});
 
 }  // namespace tint::reader::spirv
 
diff --git a/src/tint/reader/spirv/parser_impl_module_var_test.cc b/src/tint/reader/spirv/parser_impl_module_var_test.cc
index 7543792..5077a51 100644
--- a/src/tint/reader/spirv/parser_impl_module_var_test.cc
+++ b/src/tint/reader/spirv/parser_impl_module_var_test.cc
@@ -1251,7 +1251,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr(R"(type Arr = @stride(4) array<u32, 2u>;
+    EXPECT_THAT(module_str, HasSubstr(R"(alias Arr = @stride(4) array<u32, 2u>;
 
 struct S {
   /* @offset(0) */
@@ -2424,13 +2424,13 @@
     ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    const std::string expected = R"(type Arr = @stride(4) array<u32, 1u>;
+    const std::string expected = R"(alias Arr = @stride(4) array<u32, 1u>;
 
-type Arr_1 = @stride(4) array<u32, 2u>;
+alias Arr_1 = @stride(4) array<u32, 2u>;
 
-type Arr_2 = @stride(4) array<i32, 1u>;
+alias Arr_2 = @stride(4) array<i32, 1u>;
 
-type Arr_3 = @stride(4) array<i32, 2u>;
+alias Arr_3 = @stride(4) array<i32, 2u>;
 
 var<private> x_1 : Arr;
 
@@ -2463,13 +2463,13 @@
     ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    const std::string expected = R"(type Arr = @stride(4) array<u32, 1u>;
+    const std::string expected = R"(alias Arr = @stride(4) array<u32, 1u>;
 
-type Arr_1 = @stride(4) array<u32, 2u>;
+alias Arr_1 = @stride(4) array<u32, 2u>;
 
-type Arr_2 = @stride(4) array<i32, 1u>;
+alias Arr_2 = @stride(4) array<i32, 1u>;
 
-type Arr_3 = @stride(4) array<i32, 2u>;
+alias Arr_3 = @stride(4) array<i32, 2u>;
 
 var<private> x_1 : Arr;
 
diff --git a/src/tint/reader/spirv/parser_impl_named_types_test.cc b/src/tint/reader/spirv/parser_impl_named_types_test.cc
index 3fb07a4..2b29fbe 100644
--- a/src/tint/reader/spirv/parser_impl_named_types_test.cc
+++ b/src/tint/reader/spirv/parser_impl_named_types_test.cc
@@ -89,9 +89,9 @@
     %arr2 = OpTypeRuntimeArray %uint
   )"));
     EXPECT_TRUE(p->BuildAndParseInternalModule());
-    EXPECT_THAT(test::ToString(p->program()), HasSubstr(R"(type RTArr = @stride(8) array<u32>;
+    EXPECT_THAT(test::ToString(p->program()), HasSubstr(R"(alias RTArr = @stride(8) array<u32>;
 
-type RTArr_1 = @stride(8) array<u32>;
+alias RTArr_1 = @stride(8) array<u32>;
 )"));
 
     p->DeliberatelyInvalidSpirv();
@@ -135,9 +135,9 @@
     %arr2 = OpTypeArray %uint %uint_5
   )"));
     EXPECT_TRUE(p->BuildAndParseInternalModule());
-    EXPECT_THAT(test::ToString(p->program()), HasSubstr(R"(type Arr = @stride(8) array<u32, 5u>;
+    EXPECT_THAT(test::ToString(p->program()), HasSubstr(R"(alias Arr = @stride(8) array<u32, 5u>;
 
-type Arr_1 = @stride(8) array<u32, 5u>;
+alias Arr_1 = @stride(8) array<u32, 5u>;
 )"));
 
     p->DeliberatelyInvalidSpirv();
diff --git a/src/tint/reader/spirv/parser_test.cc b/src/tint/reader/spirv/parser_test.cc
index 35cb5da..9cbb461 100644
--- a/src/tint/reader/spirv/parser_test.cc
+++ b/src/tint/reader/spirv/parser_test.cc
@@ -14,7 +14,9 @@
 
 #include "src/tint/reader/spirv/parser.h"
 
+#include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "src/tint/reader/spirv/spirv_tools_helpers_test.h"
 
 namespace tint::reader::spirv {
 namespace {
@@ -29,6 +31,53 @@
     EXPECT_EQ(errs, "error: line:0: Invalid SPIR-V magic number.\n");
 }
 
+constexpr auto kShaderWithNonUniformDerivative = R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %foo "foo" %x
+               OpExecutionMode %foo OriginUpperLeft
+               OpDecorate %x Location 0
+      %float = OpTypeFloat 32
+%_ptr_Input_float = OpTypePointer Input %float
+          %x = OpVariable %_ptr_Input_float Input
+       %void = OpTypeVoid
+    %float_0 = OpConstantNull %float
+       %bool = OpTypeBool
+  %func_type = OpTypeFunction %void
+        %foo = OpFunction %void None %func_type
+  %foo_start = OpLabel
+    %x_value = OpLoad %float %x
+  %condition = OpFOrdGreaterThan %bool %x_value %float_0
+               OpSelectionMerge %merge None
+               OpBranchConditional %condition %true_branch %merge
+%true_branch = OpLabel
+     %result = OpDPdx %float %x_value
+               OpBranch %merge
+      %merge = OpLabel
+               OpReturn
+               OpFunctionEnd
+)";
+
+TEST_F(ParserTest, AllowNonUniformDerivatives_False) {
+    auto spv = test::Assemble(kShaderWithNonUniformDerivative);
+    Options options;
+    options.allow_non_uniform_derivatives = false;
+    auto program = Parse(spv, options);
+    auto errs = diag::Formatter().format(program.Diagnostics());
+    EXPECT_FALSE(program.IsValid()) << errs;
+    EXPECT_THAT(errs, ::testing::HasSubstr("'dpdx' must only be called from uniform control flow"));
+}
+
+TEST_F(ParserTest, AllowNonUniformDerivatives_True) {
+    auto spv = test::Assemble(kShaderWithNonUniformDerivative);
+    Options options;
+    options.allow_non_uniform_derivatives = true;
+    auto program = Parse(spv, options);
+    auto errs = diag::Formatter().format(program.Diagnostics());
+    EXPECT_TRUE(program.IsValid()) << errs;
+    EXPECT_EQ(program.Diagnostics().count(), 0u) << errs;
+}
+
 // TODO(dneto): uint32 vec, valid SPIR-V
 // TODO(dneto): uint32 vec, invalid SPIR-V
 
diff --git a/src/tint/reader/wgsl/classify_template_args.cc b/src/tint/reader/wgsl/classify_template_args.cc
new file mode 100644
index 0000000..136ff18
--- /dev/null
+++ b/src/tint/reader/wgsl/classify_template_args.cc
@@ -0,0 +1,168 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/reader/wgsl/classify_template_args.h"
+
+#include <vector>
+
+#include "src/tint/debug.h"
+#include "src/tint/utils/vector.h"
+
+namespace tint::reader::wgsl {
+
+namespace {
+
+/// If the token at index @p idx is a '>>', '>=' or '>>=', then the token is split into two, with
+/// the first being '>', otherwise MaybeSplit() will be a no-op.
+/// @param tokens the vector of tokens
+/// @param idx the index of the token to (maybe) split
+void MaybeSplit(std::vector<Token>& tokens, size_t idx) {
+    Token* token = &tokens[idx];
+    switch (token->type()) {
+        case Token::Type::kShiftRight:  //  '>>'
+            TINT_ASSERT(Reader, token[1].type() == Token::Type::kPlaceholder);
+            token[0].SetType(Token::Type::kGreaterThan);
+            token[1].SetType(Token::Type::kGreaterThan);
+            break;
+        case Token::Type::kGreaterThanEqual:  //  '>='
+            TINT_ASSERT(Reader, token[1].type() == Token::Type::kPlaceholder);
+            token[0].SetType(Token::Type::kGreaterThan);
+            token[1].SetType(Token::Type::kEqual);
+            break;
+        case Token::Type::kShiftRightEqual:  // '>>='
+            TINT_ASSERT(Reader, token[1].type() == Token::Type::kPlaceholder);
+            token[0].SetType(Token::Type::kGreaterThan);
+            token[1].SetType(Token::Type::kGreaterThanEqual);
+            break;
+        default:
+            break;
+    }
+}
+
+}  // namespace
+
+void ClassifyTemplateArguments(std::vector<Token>& tokens) {
+    const size_t count = tokens.size();
+
+    // The current expression nesting depth.
+    // Each '(', '[' increments the depth.
+    // Each ')', ']' decrements the depth.
+    uint64_t expr_depth = 0;
+
+    // A stack of '<' tokens.
+    // Used to pair '<' and '>' tokens at the same expression depth.
+    struct StackEntry {
+        Token* token;         // A pointer to the opening '<' token
+        uint64_t expr_depth;  // The value of 'expr_depth' for the opening '<'
+    };
+    utils::Vector<StackEntry, 16> stack;
+
+    for (size_t i = 0; i < count - 1; i++) {
+        switch (tokens[i].type()) {
+            // <identifier> + all type / builtin keywords that will become identifiers.
+            case Token::Type::kIdentifier:
+            case Token::Type::kArray:
+            case Token::Type::kAtomic:
+            case Token::Type::kBitcast:
+            case Token::Type::kMat2x2:
+            case Token::Type::kMat2x3:
+            case Token::Type::kMat2x4:
+            case Token::Type::kMat3x2:
+            case Token::Type::kMat3x3:
+            case Token::Type::kMat3x4:
+            case Token::Type::kMat4x2:
+            case Token::Type::kMat4x3:
+            case Token::Type::kMat4x4:
+            case Token::Type::kPtr:
+            case Token::Type::kTextureMultisampled2d:
+            case Token::Type::kTextureSampled1d:
+            case Token::Type::kTextureSampled2d:
+            case Token::Type::kTextureSampled2dArray:
+            case Token::Type::kTextureSampled3d:
+            case Token::Type::kTextureSampledCube:
+            case Token::Type::kTextureSampledCubeArray:
+            case Token::Type::kTextureStorage1d:
+            case Token::Type::kTextureStorage2d:
+            case Token::Type::kTextureStorage2dArray:
+            case Token::Type::kVec2:
+            case Token::Type::kVec3:
+            case Token::Type::kVec4:
+            case Token::Type::kTextureStorage3d: {
+                auto& next = tokens[i + 1];
+                if (next.type() == Token::Type::kLessThan) {
+                    // ident '<'
+                    // Push this '<' to the stack, along with the current nesting expr_depth.
+                    stack.Push(StackEntry{&tokens[i + 1], expr_depth});
+                    i++;  // Skip the '<'
+                }
+                break;
+            }
+            case Token::Type::kGreaterThan:       // '>'
+            case Token::Type::kShiftRight:        // '>>'
+            case Token::Type::kGreaterThanEqual:  // '>='
+            case Token::Type::kShiftRightEqual:   // '>>='
+                if (!stack.IsEmpty() && stack.Back().expr_depth == expr_depth) {
+                    // '<' and '>' at same expr_depth, and no terminating tokens in-between.
+                    // Consider both as a template argument list.
+                    MaybeSplit(tokens, i);
+                    stack.Pop().token->SetType(Token::Type::kTemplateArgsLeft);
+                    tokens[i].SetType(Token::Type::kTemplateArgsRight);
+                }
+                break;
+
+            case Token::Type::kParenLeft:    // '('
+            case Token::Type::kBracketLeft:  // '['
+                // Entering a nested expression
+                expr_depth++;
+                break;
+
+            case Token::Type::kParenRight:    // ')'
+            case Token::Type::kBracketRight:  // ']'
+                // Exiting a nested expression
+                // Pop the stack until we return to the current expression expr_depth
+                while (!stack.IsEmpty() && stack.Back().expr_depth == expr_depth) {
+                    stack.Pop();
+                }
+                if (expr_depth > 0) {
+                    expr_depth--;
+                }
+                break;
+
+            case Token::Type::kSemicolon:  // ';'
+            case Token::Type::kBraceLeft:  // '{'
+            case Token::Type::kEqual:      // '='
+            case Token::Type::kColon:      // ':'
+                // Expression terminating tokens. No opening template list can hold these tokens, so
+                // clear the stack and expression depth.
+                expr_depth = 0;
+                stack.Clear();
+                break;
+
+            case Token::Type::kOrOr:    // '||'
+            case Token::Type::kAndAnd:  // '&&'
+                // Treat 'a < b || c > d' as a logical binary operator of two comparison operators
+                // instead of a single template argument 'b||c'.
+                // Use parentheses around 'b||c' to parse as a template argument list.
+                while (!stack.IsEmpty() && stack.Back().expr_depth == expr_depth) {
+                    stack.Pop();
+                }
+                break;
+
+            default:
+                break;
+        }
+    }
+}
+
+}  // namespace tint::reader::wgsl
diff --git a/src/tint/reader/wgsl/classify_template_args.h b/src/tint/reader/wgsl/classify_template_args.h
new file mode 100644
index 0000000..92d3eb7
--- /dev/null
+++ b/src/tint/reader/wgsl/classify_template_args.h
@@ -0,0 +1,28 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_READER_WGSL_CLASSIFY_TEMPLATE_ARGS_H_
+#define SRC_TINT_READER_WGSL_CLASSIFY_TEMPLATE_ARGS_H_
+
+#include <vector>
+
+#include "src/tint/reader/wgsl/token.h"
+
+namespace tint::reader::wgsl {
+
+void ClassifyTemplateArguments(std::vector<Token>& tokens);
+
+}  // namespace tint::reader::wgsl
+
+#endif  // SRC_TINT_READER_WGSL_CLASSIFY_TEMPLATE_ARGS_H_
diff --git a/src/tint/reader/wgsl/classify_template_args_test.cc b/src/tint/reader/wgsl/classify_template_args_test.cc
new file mode 100644
index 0000000..fb9bcf9
--- /dev/null
+++ b/src/tint/reader/wgsl/classify_template_args_test.cc
@@ -0,0 +1,483 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gmock/gmock.h"
+
+#include "src/tint/reader/wgsl/classify_template_args.h"
+#include "src/tint/reader/wgsl/lexer.h"
+#include "src/tint/utils/transform.h"
+
+namespace tint::reader::wgsl {
+namespace {
+
+using T = Token::Type;
+
+struct Case {
+    const char* wgsl;
+    std::vector<T> tokens;
+};
+
+static std::ostream& operator<<(std::ostream& out, const Case& c) {
+    return out << "'" << c.wgsl << "'";
+}
+
+using ClassifyTemplateArgsTest = testing::TestWithParam<Case>;
+
+TEST_P(ClassifyTemplateArgsTest, Classify) {
+    auto& params = GetParam();
+    Source::File file("", params.wgsl);
+    Lexer l(&file);
+    auto tokens = l.Lex();
+    ClassifyTemplateArguments(tokens);
+    auto types = utils::Transform(tokens, [&](const Token& t) { return t.type(); });
+    EXPECT_THAT(types, testing::ContainerEq(params.tokens));
+}
+
+INSTANTIATE_TEST_SUITE_P(NonTemplate,
+                         ClassifyTemplateArgsTest,
+                         testing::ValuesIn(std::vector<Case>{
+                             {
+                                 "",
+                                 {T::kEOF},
+                             },
+                             {
+                                 "abc",
+                                 {T::kIdentifier, T::kEOF},
+                             },
+                             {
+                                 "a<b",
+                                 {T::kIdentifier, T::kLessThan, T::kIdentifier, T::kEOF},
+                             },
+                             {
+                                 "a>b",
+                                 {T::kIdentifier, T::kGreaterThan, T::kIdentifier, T::kEOF},
+                             },
+                             {
+                                 "(a<b)>c",
+                                 {
+                                     T::kParenLeft,    // (
+                                     T::kIdentifier,   // a
+                                     T::kLessThan,     // <
+                                     T::kIdentifier,   // b
+                                     T::kParenRight,   // )
+                                     T::kGreaterThan,  // >
+                                     T::kIdentifier,   // c
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<(b>c)",
+                                 {
+                                     T::kIdentifier,   // a
+                                     T::kLessThan,     // <
+                                     T::kParenLeft,    // (
+                                     T::kIdentifier,   // b
+                                     T::kGreaterThan,  // >
+                                     T::kIdentifier,   // c
+                                     T::kParenRight,   // )
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a((b<c), d>(e))",
+                                 {
+                                     T::kIdentifier,   // a
+                                     T::kParenLeft,    // (
+                                     T::kParenLeft,    // (
+                                     T::kIdentifier,   // b
+                                     T::kLessThan,     // <
+                                     T::kIdentifier,   // c
+                                     T::kParenRight,   // )
+                                     T::kComma,        // ,
+                                     T::kIdentifier,   // d
+                                     T::kGreaterThan,  // >
+                                     T::kParenLeft,    // (
+                                     T::kIdentifier,   // e
+                                     T::kParenRight,   // )
+                                     T::kParenRight,   // )
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b[c>(d)]",
+                                 {
+                                     T::kIdentifier,    // a
+                                     T::kLessThan,      // <
+                                     T::kIdentifier,    // b
+                                     T::kBracketLeft,   // [
+                                     T::kIdentifier,    // c
+                                     T::kGreaterThan,   // >
+                                     T::kParenLeft,     // (
+                                     T::kIdentifier,    // d
+                                     T::kParenRight,    // )
+                                     T::kBracketRight,  // ]
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b;c>d()",
+                                 {
+                                     T::kIdentifier,   // a
+                                     T::kLessThan,     // <
+                                     T::kIdentifier,   // b
+                                     T::kSemicolon,    // ;
+                                     T::kIdentifier,   // c
+                                     T::kGreaterThan,  // >
+                                     T::kIdentifier,   // d
+                                     T::kParenLeft,    // (
+                                     T::kParenRight,   // )
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "if a < b {} else if c > d {}",
+                                 {
+                                     T::kIf,           // a
+                                     T::kIdentifier,   // a
+                                     T::kLessThan,     // <
+                                     T::kIdentifier,   // b
+                                     T::kBraceLeft,    // {
+                                     T::kBraceRight,   // }
+                                     T::kElse,         // else
+                                     T::kIf,           // if
+                                     T::kIdentifier,   // c
+                                     T::kGreaterThan,  // >
+                                     T::kIdentifier,   // d
+                                     T::kBraceLeft,    // {
+                                     T::kBraceRight,   // }
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b&&c>d",
+                                 {
+                                     T::kIdentifier,   // a
+                                     T::kLessThan,     // <
+                                     T::kIdentifier,   // b
+                                     T::kAndAnd,       // &&
+                                     T::kPlaceholder,  // <placeholder>
+                                     T::kIdentifier,   // c
+                                     T::kGreaterThan,  // >
+                                     T::kIdentifier,   // d
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b||c>d",
+                                 {
+                                     T::kIdentifier,   // a
+                                     T::kLessThan,     // <
+                                     T::kIdentifier,   // b
+                                     T::kOrOr,         // ||
+                                     T::kIdentifier,   // c
+                                     T::kGreaterThan,  // >
+                                     T::kIdentifier,   // d
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b<c||d>>",
+                                 {
+                                     T::kIdentifier,   // a
+                                     T::kLessThan,     // <
+                                     T::kIdentifier,   // b
+                                     T::kLessThan,     // <
+                                     T::kIdentifier,   // c
+                                     T::kOrOr,         // ||
+                                     T::kIdentifier,   // d
+                                     T::kShiftRight,   // >>
+                                     T::kPlaceholder,  // <placeholder>
+                                     T::kEOF,
+                                 },
+                             },
+                         }));
+
+INSTANTIATE_TEST_SUITE_P(Template,
+                         ClassifyTemplateArgsTest,
+                         testing::ValuesIn(std::vector<Case>{
+                             {
+                                 "a<b>()",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kTemplateArgsRight,  // >
+                                     T::kParenLeft,          // (
+                                     T::kParenRight,         // )
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b>c",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kTemplateArgsRight,  // >
+                                     T::kIdentifier,         // c
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "vec3<i32>",
+                                 {
+                                     T::kVec3,               // vec3
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kI32,                // i32
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "vec3<i32>()",
+                                 {
+                                     T::kVec3,               // vec3
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kI32,                // i32
+                                     T::kTemplateArgsRight,  // >
+                                     T::kParenLeft,          // (
+                                     T::kParenRight,         // )
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "array<vec3<i32>,5>",
+                                 {
+                                     T::kArray,              // array
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kVec3,               // vec3
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kI32,                // i32
+                                     T::kTemplateArgsRight,  // >
+                                     T::kComma,              // ,
+                                     T::kIntLiteral,         // 5
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a(b<c, d>(e))",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kParenLeft,          // (
+                                     T::kIdentifier,         // b
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // c
+                                     T::kComma,              // ,
+                                     T::kIdentifier,         // d
+                                     T::kTemplateArgsRight,  // >
+                                     T::kParenLeft,          // (
+                                     T::kIdentifier,         // e
+                                     T::kParenRight,         // )
+                                     T::kParenRight,         // )
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<1+2>()",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIntLiteral,         // 1
+                                     T::kPlus,               // +
+                                     T::kIntLiteral,         // 2
+                                     T::kTemplateArgsRight,  // >
+                                     T::kParenLeft,          // (
+                                     T::kParenRight,         // )
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<1,b>()",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIntLiteral,         // 1
+                                     T::kComma,              // ,
+                                     T::kIdentifier,         // b
+                                     T::kTemplateArgsRight,  // >
+                                     T::kParenLeft,          // (
+                                     T::kParenRight,         // )
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b,c>=d",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kComma,              // ,
+                                     T::kIdentifier,         // c
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEqual,              // =
+                                     T::kIdentifier,         // d
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b,c>=d>()",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kComma,              // ,
+                                     T::kIdentifier,         // c
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEqual,              // =
+                                     T::kIdentifier,         // d
+                                     T::kGreaterThan,        // >
+                                     T::kParenLeft,          // (
+                                     T::kParenRight,         // )
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b<c>>=",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // c
+                                     T::kTemplateArgsRight,  // >
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEqual,              // =
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b>c>()",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kTemplateArgsRight,  // >
+                                     T::kIdentifier,         // c
+                                     T::kGreaterThan,        // >
+                                     T::kParenLeft,          // (
+                                     T::kParenRight,         // )
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b<c>()",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kLessThan,           // <
+                                     T::kIdentifier,         // b
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // c
+                                     T::kTemplateArgsRight,  // >
+                                     T::kParenLeft,          // (
+                                     T::kParenRight,         // )
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b<c>>()",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // c
+                                     T::kTemplateArgsRight,  // >
+                                     T::kTemplateArgsRight,  // >
+                                     T::kParenLeft,          // (
+                                     T::kParenRight,         // )
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b<c>()>()",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // c
+                                     T::kTemplateArgsRight,  // >
+                                     T::kParenLeft,          // (
+                                     T::kParenRight,         // )
+                                     T::kTemplateArgsRight,  // >
+                                     T::kParenLeft,          // (
+                                     T::kParenRight,         // )
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b>.c",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kTemplateArgsRight,  // >
+                                     T::kPeriod,             // .
+                                     T::kIdentifier,         // c
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<(b&&c)>d",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kParenLeft,          // (
+                                     T::kIdentifier,         // b
+                                     T::kAndAnd,             // &&
+                                     T::kPlaceholder,        // <placeholder>
+                                     T::kIdentifier,         // c
+                                     T::kParenRight,         // )
+                                     T::kTemplateArgsRight,  // >
+                                     T::kIdentifier,         // d
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<(b||c)>d",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kParenLeft,          // (
+                                     T::kIdentifier,         // b
+                                     T::kOrOr,               // ||
+                                     T::kIdentifier,         // c
+                                     T::kParenRight,         // )
+                                     T::kTemplateArgsRight,  // >
+                                     T::kIdentifier,         // d
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b<(c||d)>>",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kParenLeft,          // (
+                                     T::kIdentifier,         // c
+                                     T::kOrOr,               // ||
+                                     T::kIdentifier,         // d
+                                     T::kParenRight,         // )
+                                     T::kTemplateArgsRight,  // >
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEOF,
+                                 },
+                             },
+                         }));
+
+}  // namespace
+}  // namespace tint::reader::wgsl
diff --git a/src/tint/reader/wgsl/lexer.cc b/src/tint/reader/wgsl/lexer.cc
index 6371db7..722058c 100644
--- a/src/tint/reader/wgsl/lexer.cc
+++ b/src/tint/reader/wgsl/lexer.cc
@@ -1115,6 +1115,9 @@
 }
 
 Token Lexer::check_keyword(const Source& source, std::string_view str) {
+    if (str == "alias") {
+        return {Token::Type::kAlias, source, "alias"};
+    }
     if (str == "array") {
         return {Token::Type::kArray, source, "array"};
     }
@@ -1136,6 +1139,9 @@
     if (str == "const") {
         return {Token::Type::kConst, source, "const"};
     }
+    if (str == "const_assert") {
+        return {Token::Type::kConstAssert, source, "const_assert"};
+    }
     if (str == "continue") {
         return {Token::Type::kContinue, source, "continue"};
     }
diff --git a/src/tint/reader/wgsl/lexer_test.cc b/src/tint/reader/wgsl/lexer_test.cc
index a1a4764..a376888 100644
--- a/src/tint/reader/wgsl/lexer_test.cc
+++ b/src/tint/reader/wgsl/lexer_test.cc
@@ -1057,12 +1057,14 @@
 INSTANTIATE_TEST_SUITE_P(
     LexerTest,
     KeywordTest,
-    testing::Values(TokenData{"array", Token::Type::kArray},
+    testing::Values(TokenData{"alias", Token::Type::kAlias},
+                    TokenData{"array", Token::Type::kArray},
                     TokenData{"bitcast", Token::Type::kBitcast},
                     TokenData{"bool", Token::Type::kBool},
                     TokenData{"break", Token::Type::kBreak},
                     TokenData{"case", Token::Type::kCase},
                     TokenData{"const", Token::Type::kConst},
+                    TokenData{"const_assert", Token::Type::kConstAssert},
                     TokenData{"continue", Token::Type::kContinue},
                     TokenData{"continuing", Token::Type::kContinuing},
                     TokenData{"default", Token::Type::kDefault},
diff --git a/src/tint/reader/wgsl/parser_impl.cc b/src/tint/reader/wgsl/parser_impl.cc
index 620dc1e..37c3ac3 100644
--- a/src/tint/reader/wgsl/parser_impl.cc
+++ b/src/tint/reader/wgsl/parser_impl.cc
@@ -38,6 +38,7 @@
 #include "src/tint/ast/variable_decl_statement.h"
 #include "src/tint/ast/vector.h"
 #include "src/tint/ast/workgroup_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"
 #include "src/tint/type/external_texture.h"
@@ -319,6 +320,7 @@
 void ParserImpl::InitializeLex() {
     Lexer l{file_};
     tokens_ = l.Lex();
+    ClassifyTemplateArguments(tokens_);
 }
 
 bool ParserImpl::Parse() {
@@ -358,14 +360,47 @@
 }
 
 // global_directive
-//  : enable_directive
+//  : diagnostic_directive
+//  | enable_directive
 Maybe<Void> ParserImpl::global_directive(bool have_parsed_decl) {
     auto& p = peek();
-    auto ed = enable_directive();
-    if (ed.matched && have_parsed_decl) {
-        return add_error(p, "enable directives must come before all global declarations");
+    Maybe<Void> result = diagnostic_directive();
+    if (!result.errored && !result.matched) {
+        result = enable_directive();
     }
-    return ed;
+
+    if (result.matched && have_parsed_decl) {
+        return add_error(p, "directives must come before all global declarations");
+    }
+    return result;
+}
+
+// diagnostic_directive
+//  : diagnostic diagnostic_control SEMICOLON
+Maybe<Void> ParserImpl::diagnostic_directive() {
+    auto decl = sync(Token::Type::kSemicolon, [&]() -> Maybe<Void> {
+        if (!match(Token::Type::kDiagnostic)) {
+            return Failure::kNoMatch;
+        }
+
+        auto control = expect_diagnostic_control();
+        if (control.errored) {
+            return Failure::kErrored;
+        }
+
+        if (!expect("diagnostic directive", Token::Type::kSemicolon)) {
+            return Failure::kErrored;
+        }
+
+        builder_.AST().AddDiagnosticControl(std::move(control.value));
+
+        return kSuccess;
+    });
+
+    if (decl.errored) {
+        return Failure::kErrored;
+    }
+    return decl;
 }
 
 // enable_directive
@@ -427,7 +462,7 @@
 //  | type_alias_decl SEMICOLON
 //  | struct_decl
 //  | function_decl
-//  | static_assert_statement SEMICOLON
+//  | const_assert_statement SEMICOLON
 Maybe<Void> ParserImpl::global_decl() {
     if (match(Token::Type::kSemicolon) || match(Token::Type::kEOF)) {
         return kSuccess;
@@ -486,13 +521,13 @@
             return kSuccess;
         }
 
-        auto assertion = static_assert_statement();
+        auto assertion = const_assert_statement();
         if (assertion.errored) {
             return Failure::kErrored;
         }
         if (assertion.matched) {
-            builder_.AST().AddStaticAssert(assertion.value);
-            if (!expect("static assertion declaration", Token::Type::kSemicolon)) {
+            builder_.AST().AddConstAssert(assertion.value);
+            if (!expect("const assertion declaration", Token::Type::kSemicolon)) {
                 return Failure::kErrored;
             }
             return kSuccess;
@@ -731,7 +766,7 @@
     if (dim.matched) {
         const char* use = "sampled texture type";
 
-        auto subtype = expect_lt_gt_block(use, [&] { return expect_type(use); });
+        auto subtype = expect_template_arg_block(use, [&] { return expect_type(use); });
         if (subtype.errored) {
             return Failure::kErrored;
         }
@@ -743,7 +778,7 @@
     if (ms_dim.matched) {
         const char* use = "multisampled texture type";
 
-        auto subtype = expect_lt_gt_block(use, [&] { return expect_type(use); });
+        auto subtype = expect_template_arg_block(use, [&] { return expect_type(use); });
         if (subtype.errored) {
             return Failure::kErrored;
         }
@@ -755,7 +790,7 @@
     if (storage.matched) {
         const char* use = "storage texture type";
         using StorageTextureInfo = std::pair<tint::type::TexelFormat, tint::type::Access>;
-        auto params = expect_lt_gt_block(use, [&]() -> Expect<StorageTextureInfo> {
+        auto params = expect_template_arg_block(use, [&]() -> Expect<StorageTextureInfo> {
             auto format = expect_texel_format(use);
             if (format.errored) {
                 return Failure::kErrored;
@@ -1008,13 +1043,18 @@
 }
 
 // type_alias_decl
-//   : TYPE IDENT EQUAL type_specifier
+//   : ALIAS IDENT EQUAL type_specifier
 Maybe<const ast::Alias*> ParserImpl::type_alias_decl() {
-    if (!peek_is(Token::Type::kType)) {
+    Source source;
+    if (match(Token::Type::kAlias, &source)) {
+        // matched.
+    } else if (match(Token::Type::kType, &source)) {
+        // TODO(crbug.com/tint/1812): DEPRECATED
+        deprecated(source, "'type' has been renamed to 'alias'");
+    } else {
         return Failure::kNoMatch;
     }
 
-    auto& t = next();
     const char* use = "type alias";
 
     auto name = expect_ident(use);
@@ -1034,7 +1074,7 @@
         return add_error(peek(), "invalid type alias");
     }
 
-    return builder_.ty.alias(make_source_range_from(t.source()), name.value, type.value);
+    return builder_.ty.alias(make_source_range_from(source), name.value, type.value);
 }
 
 // vec_prefix
@@ -1124,7 +1164,8 @@
         return builder_.ty.u32(t.source());
     }
 
-    if (t.Is(Token::Type::kArray) && peek_is(Token::Type::kLessThan, 1)) {
+    if (t.Is(Token::Type::kArray) &&
+        (peek_is(Token::Type::kTemplateArgsLeft, 1) || peek_is(Token::Type::kLessThan, 1))) {
         if (match(Token::Type::kArray)) {
             return expect_type_specifier_array(t.source());
         }
@@ -1138,14 +1179,16 @@
         return expect_type_specifier_pointer(t.source());
     }
 
-    if (t.IsMatrix() && peek_is(Token::Type::kLessThan, 1)) {
+    if (t.IsMatrix() &&
+        (peek_is(Token::Type::kTemplateArgsLeft, 1) || peek_is(Token::Type::kLessThan, 1))) {
         auto mat = mat_prefix();
         if (mat.matched) {
             return expect_type_specifier_matrix(t.source(), mat.value);
         }
     }
 
-    if (t.IsVector() && peek_is(Token::Type::kLessThan, 1)) {
+    if (t.IsVector() &&
+        (peek_is(Token::Type::kTemplateArgsLeft, 1) || peek_is(Token::Type::kLessThan, 1))) {
         auto vec = vec_prefix();
         if (vec.matched) {
             return expect_type_specifier_vector(t.source(), vec.value);
@@ -1254,7 +1297,7 @@
     auto address_space = type::AddressSpace::kNone;
     auto access = type::Access::kUndefined;
 
-    auto subtype = expect_lt_gt_block(use, [&]() -> Expect<const ast::Type*> {
+    auto subtype = expect_template_arg_block(use, [&]() -> Expect<const ast::Type*> {
         auto sc = expect_address_space(use);
         if (sc.errored) {
             return Failure::kErrored;
@@ -1292,7 +1335,7 @@
 Expect<const ast::Type*> ParserImpl::expect_type_specifier_atomic(const Source& s) {
     const char* use = "atomic declaration";
 
-    auto subtype = expect_lt_gt_block(use, [&] { return expect_type(use); });
+    auto subtype = expect_template_arg_block(use, [&] { return expect_type(use); });
     if (subtype.errored) {
         return Failure::kErrored;
     }
@@ -1303,7 +1346,7 @@
 // LESS_THAN type_specifier GREATER_THAN
 Expect<const ast::Type*> ParserImpl::expect_type_specifier_vector(const Source& s, uint32_t count) {
     const char* use = "vector";
-    auto ty = expect_lt_gt_block(use, [&] { return expect_type(use); });
+    auto ty = expect_template_arg_block(use, [&] { return expect_type(use); });
     if (ty.errored) {
         return Failure::kErrored;
     }
@@ -1320,11 +1363,7 @@
         const ast::Expression* size = nullptr;
     };
 
-    if (!peek_is(Token::Type::kLessThan)) {
-        return add_error(peek(), "expected < for array");
-    }
-
-    auto type_size = expect_lt_gt_block(use, [&]() -> Expect<TypeAndSize> {
+    auto type_size = expect_template_arg_block(use, [&]() -> Expect<TypeAndSize> {
         auto type = expect_type(use);
         if (type.errored) {
             return Failure::kErrored;
@@ -1356,7 +1395,7 @@
 Expect<const ast::Type*> ParserImpl::expect_type_specifier_matrix(const Source& s,
                                                                   const MatrixDimensions& dims) {
     const char* use = "matrix";
-    auto ty = expect_lt_gt_block(use, [&] { return expect_type(use); });
+    auto ty = expect_template_arg_block(use, [&] { return expect_type(use); });
     if (ty.errored) {
         return Failure::kErrored;
     }
@@ -1450,11 +1489,15 @@
                                      decl->type, std::move(attrs.value));
 }
 
-// static_assert_statement
+// const_assert_statement
 //   : STATIC_ASSERT expression
-Maybe<const ast::StaticAssert*> ParserImpl::static_assert_statement() {
+Maybe<const ast::ConstAssert*> ParserImpl::const_assert_statement() {
     Source start;
-    if (!match(Token::Type::kStaticAssert, &start)) {
+    if (match(Token::Type::kConstAssert, &start)) {
+        // matched
+    } else if (match(Token::Type::kStaticAssert, &start)) {
+        deprecated(start, "'static_assert' has been renamed to 'const_assert'");
+    } else {
         return Failure::kNoMatch;
     }
 
@@ -1467,7 +1510,7 @@
     }
 
     Source source = make_source_range_from(start);
-    return create<ast::StaticAssert>(source, condition.value);
+    return create<ast::ConstAssert>(source, condition.value);
 }
 
 // function_decl
@@ -1781,7 +1824,7 @@
 //   | continue_statement SEMICOLON
 //   | DISCARD SEMICOLON
 //   | variable_updating_statement SEMICOLON
-//   | static_assert_statement SEMICOLON
+//   | const_assert_statement SEMICOLON
 Maybe<const ast::Statement*> ParserImpl::non_block_statement() {
     auto stmt = [&]() -> Maybe<const ast::Statement*> {
         auto ret_stmt = return_statement();
@@ -1838,7 +1881,7 @@
             return assign.value;
         }
 
-        auto stmt_static_assert = static_assert_statement();
+        auto stmt_static_assert = const_assert_statement();
         if (stmt_static_assert.errored) {
             return Failure::kErrored;
         }
@@ -2546,7 +2589,7 @@
     if (match(Token::Type::kBitcast)) {
         const char* use = "bitcast expression";
 
-        auto type = expect_lt_gt_block(use, [&] { return expect_type(use); });
+        auto type = expect_template_arg_block(use, [&] { return expect_type(use); });
         if (type.errored) {
             return Failure::kErrored;
         }
@@ -2583,6 +2626,14 @@
     if (t.IsIdentifier()) {
         next();
 
+        if (Source source; match(Token::Type::kTemplateArgsLeft, &source)) {
+            return add_error(
+                source,
+                "'<' treated as the start of a template argument list, which is not supported for "
+                "user-declared types or functions. If you intended less-than, wrap the expression "
+                "in parentheses");
+        }
+
         auto* ident =
             create<ast::IdentifierExpression>(t.source(), builder_.Symbols().Register(t.to_str()));
 
@@ -3452,6 +3503,7 @@
 //   | ATTR 'binding' PAREN_LEFT expression COMMA? PAREN_RIGHT
 //   | ATTR 'builtin' PAREN_LEFT builtin_value_name COMMA? PAREN_RIGHT
 //   | ATTR 'const'
+//   | ATTR 'diagnostic' diagnostic_control
 //   | ATTR 'group' PAREN_LEFT expression COMMA? PAREN_RIGHT
 //   | ATTR 'id' PAREN_LEFT expression COMMA? PAREN_RIGHT
 //   | ATTR 'interpolate' PAREN_LEFT interpolation_type_name COMMA? PAREN_RIGHT
@@ -3471,7 +3523,7 @@
     using Result = Maybe<const ast::Attribute*>;
     auto& t = next();
 
-    if (!t.IsIdentifier()) {
+    if (!t.IsIdentifier() && !(t.Is(Token::Type::kDiagnostic))) {
         return Failure::kNoMatch;
     }
 
@@ -3525,6 +3577,14 @@
 
     // Note, `const` is not valid in a WGSL source file, it's internal only
 
+    if (t.Is(Token::Type::kDiagnostic)) {
+        auto control = expect_diagnostic_control();
+        if (control.errored) {
+            return Failure::kErrored;
+        }
+        return create<ast::DiagnosticAttribute>(t.source(), control.value);
+    }
+
     if (t == "fragment") {
         return create<ast::StageAttribute>(t.source(), ast::PipelineStage::kFragment);
     }
@@ -3679,6 +3739,43 @@
     return false;
 }
 
+// severity_control_name
+//   : 'error'
+//   | 'warning'
+//   | 'info'
+//   | 'off'
+Expect<ast::DiagnosticSeverity> ParserImpl::expect_severity_control_name() {
+    return expect_enum("severity control", ast::ParseDiagnosticSeverity,
+                       ast::kDiagnosticSeverityStrings);
+}
+
+// diagnostic_control
+// : PAREN_LEFT severity_control_name COMMA ident_pattern_token COMMA ? PAREN_RIGHT
+Expect<const ast::DiagnosticControl*> ParserImpl::expect_diagnostic_control() {
+    auto source = last_source();
+    return expect_paren_block("diagnostic control", [&]() -> Expect<const ast::DiagnosticControl*> {
+        auto severity_control = expect_severity_control_name();
+        if (severity_control.errored) {
+            return Failure::kErrored;
+        }
+
+        if (!expect("diagnostic control", Token::Type::kComma)) {
+            return Failure::kErrored;
+        }
+
+        auto rule_name = expect_ident("diagnostic control");
+        if (rule_name.errored) {
+            return Failure::kErrored;
+        }
+        match(Token::Type::kComma);
+
+        return create<ast::DiagnosticControl>(
+            source, severity_control.value,
+            create<ast::IdentifierExpression>(rule_name.source,
+                                              builder_.Symbols().Register(rule_name.value)));
+    });
+}
+
 bool ParserImpl::match(Token::Type tok, Source* source /*= nullptr*/) {
     auto& t = peek();
 
@@ -3724,7 +3821,11 @@
     }
 
     std::stringstream err;
-    err << "expected '" << Token::TypeToName(tok) << "'";
+    if (tok == Token::Type::kTemplateArgsLeft && t.type() == Token::Type::kLessThan) {
+        err << "missing closing '>'";
+    } else {
+        err << "expected '" << Token::TypeToName(tok) << "'";
+    }
     if (!use.empty()) {
         err << " for " << use;
     }
@@ -3834,6 +3935,12 @@
 }
 
 template <typename F, typename T>
+T ParserImpl::expect_template_arg_block(std::string_view use, F&& body) {
+    return expect_block(Token::Type::kTemplateArgsLeft, Token::Type::kTemplateArgsRight, use,
+                        std::forward<F>(body));
+}
+
+template <typename F, typename T>
 T ParserImpl::sync(Token::Type tok, F&& body) {
     if (parse_depth_ >= kMaxParseDepth) {
         // We've hit a maximum parser recursive depth.
diff --git a/src/tint/reader/wgsl/parser_impl.h b/src/tint/reader/wgsl/parser_impl.h
index 4ef19bb..8101261 100644
--- a/src/tint/reader/wgsl/parser_impl.h
+++ b/src/tint/reader/wgsl/parser_impl.h
@@ -402,6 +402,9 @@
     /// @param has_parsed_decl flag indicating if the parser has consumed a global declaration.
     /// @return true on parse success, otherwise an error or no-match.
     Maybe<Void> global_directive(bool has_parsed_decl);
+    /// Parses the `diagnostic_directive` grammar element, erroring on parse failure.
+    /// @return true on parse success, otherwise an error or no-match.
+    Maybe<Void> diagnostic_directive();
     /// Parses the `enable_directive` grammar element, erroring on parse failure.
     /// @return true on parse success, otherwise an error or no-match.
     Maybe<Void> enable_directive();
@@ -505,9 +508,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<type::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_statement();
+    /// Parses a `const_assert_statement` grammar element
+    /// @returns returns the const assert, if it matched.
+    Maybe<const ast::ConstAssert*> const_assert_statement();
     /// Parses a `function_header` grammar element
     /// @returns the parsed function header
     Maybe<FunctionHeader> function_header();
@@ -702,6 +705,12 @@
     /// @see #attribute for the full list of attributes this method parses.
     /// @return the parsed attribute, or nullptr on error.
     Expect<const ast::Attribute*> expect_attribute();
+    /// Parses a severity_control_name grammar element.
+    /// @return the parsed severity control name, or nullptr on error.
+    Expect<ast::DiagnosticSeverity> expect_severity_control_name();
+    /// Parses a diagnostic_control grammar element.
+    /// @return the parsed diagnostic control, or nullptr on error.
+    Expect<const ast::DiagnosticControl*> expect_diagnostic_control();
 
     /// Splits a peekable token into to parts filling in the peekable fields.
     /// @param lhs the token to set in the current position
@@ -799,6 +808,16 @@
     /// an Expect with error state.
     template <typename F, typename T = ReturnType<F>>
     T expect_lt_gt_block(std::string_view use, F&& body);
+    /// A convenience function that calls `expect_block` passing
+    /// `Token::Type::kTemplateArgsLeft` and `Token::Type::kTemplateArgsRight` for the `start` and
+    /// `end` arguments, respectively.
+    /// @param use a description of what was being parsed if an error was raised
+    /// @param body a function or lambda that is called to parse the lexical block body, with the
+    /// signature: `Expect<Result>()` or `Maybe<Result>()`.
+    /// @return the value returned by `body` if no errors are raised, otherwise an Expect with error
+    /// state.
+    template <typename F, typename T = ReturnType<F>>
+    T expect_template_arg_block(std::string_view use, F&& body);
 
     /// sync() calls the function `func`, and attempts to resynchronize the
     /// parser to the next found resynchronization token if `func` fails. If the
diff --git a/src/tint/reader/wgsl/parser_impl_diagnostic_attribute_test.cc b/src/tint/reader/wgsl/parser_impl_diagnostic_attribute_test.cc
new file mode 100644
index 0000000..aebd1a0
--- /dev/null
+++ b/src/tint/reader/wgsl/parser_impl_diagnostic_attribute_test.cc
@@ -0,0 +1,36 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/reader/wgsl/parser_impl_test_helper.h"
+
+#include "src/tint/ast/diagnostic_control.h"
+
+namespace tint::reader::wgsl {
+namespace {
+
+TEST_F(ParserImplTest, DiagnosticAttribute_Valid) {
+    auto p = parser("diagnostic(off, foo)");
+    auto a = p->attribute();
+    EXPECT_FALSE(p->has_error()) << p->error();
+    EXPECT_TRUE(a.matched);
+    auto* d = a.value->As<ast::DiagnosticAttribute>();
+    ASSERT_NE(d, nullptr);
+    EXPECT_EQ(d->control->severity, ast::DiagnosticSeverity::kOff);
+    auto* r = As<ast::IdentifierExpression>(d->control->rule_name);
+    ASSERT_NE(r, nullptr);
+    EXPECT_EQ(p->builder().Symbols().NameFor(r->symbol), "foo");
+}
+
+}  // namespace
+}  // namespace tint::reader::wgsl
diff --git a/src/tint/reader/wgsl/parser_impl_diagnostic_control_test.cc b/src/tint/reader/wgsl/parser_impl_diagnostic_control_test.cc
new file mode 100644
index 0000000..36de427
--- /dev/null
+++ b/src/tint/reader/wgsl/parser_impl_diagnostic_control_test.cc
@@ -0,0 +1,119 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/reader/wgsl/parser_impl_test_helper.h"
+
+#include "src/tint/ast/diagnostic_control.h"
+
+namespace tint::reader::wgsl {
+namespace {
+
+using SeverityPair = std::pair<std::string, ast::DiagnosticSeverity>;
+class DiagnosticControlParserTest : public ParserImplTestWithParam<SeverityPair> {};
+
+TEST_P(DiagnosticControlParserTest, DiagnosticControl_Valid) {
+    auto& params = GetParam();
+    auto p = parser("(" + params.first + ", foo)");
+    auto e = p->expect_diagnostic_control();
+    EXPECT_FALSE(e.errored);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(e.value, nullptr);
+    ASSERT_TRUE(e->Is<ast::DiagnosticControl>());
+    EXPECT_EQ(e->severity, params.second);
+
+    auto* r = As<ast::IdentifierExpression>(e->rule_name);
+    ASSERT_NE(r, nullptr);
+    EXPECT_EQ(p->builder().Symbols().NameFor(r->symbol), "foo");
+}
+INSTANTIATE_TEST_SUITE_P(DiagnosticControlParserTest,
+                         DiagnosticControlParserTest,
+                         testing::Values(SeverityPair{"error", ast::DiagnosticSeverity::kError},
+                                         SeverityPair{"warning", ast::DiagnosticSeverity::kWarning},
+                                         SeverityPair{"info", ast::DiagnosticSeverity::kInfo},
+                                         SeverityPair{"off", ast::DiagnosticSeverity::kOff}));
+
+TEST_F(ParserImplTest, DiagnosticControl_Valid_TrailingComma) {
+    auto p = parser("(error, foo,)");
+    auto e = p->expect_diagnostic_control();
+    EXPECT_FALSE(e.errored);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(e.value, nullptr);
+    ASSERT_TRUE(e->Is<ast::DiagnosticControl>());
+    EXPECT_EQ(e->severity, ast::DiagnosticSeverity::kError);
+
+    auto* r = As<ast::IdentifierExpression>(e->rule_name);
+    ASSERT_NE(r, nullptr);
+    EXPECT_EQ(p->builder().Symbols().NameFor(r->symbol), "foo");
+}
+
+TEST_F(ParserImplTest, DiagnosticControl_MissingOpenParen) {
+    auto p = parser("off, foo)");
+    auto e = p->expect_diagnostic_control();
+    EXPECT_TRUE(e.errored);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), R"(1:1: expected '(' for diagnostic control)");
+}
+
+TEST_F(ParserImplTest, DiagnosticControl_MissingCloseParen) {
+    auto p = parser("(off, foo");
+    auto e = p->expect_diagnostic_control();
+    EXPECT_TRUE(e.errored);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), R"(1:10: expected ')' for diagnostic control)");
+}
+
+TEST_F(ParserImplTest, DiagnosticControl_MissingDiagnosticSeverity) {
+    auto p = parser("(, foo");
+    auto e = p->expect_diagnostic_control();
+    EXPECT_TRUE(e.errored);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), R"(1:2: expected severity control
+Possible values: 'error', 'info', 'off', 'warning')");
+}
+
+TEST_F(ParserImplTest, DiagnosticControl_InvalidDiagnosticSeverity) {
+    auto p = parser("(fatal, foo)");
+    auto e = p->expect_diagnostic_control();
+    EXPECT_TRUE(e.errored);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), R"(1:2: expected severity control
+Possible values: 'error', 'info', 'off', 'warning')");
+}
+
+TEST_F(ParserImplTest, DiagnosticControl_MissingComma) {
+    auto p = parser("(off foo");
+    auto e = p->expect_diagnostic_control();
+    EXPECT_TRUE(e.errored);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), R"(1:6: expected ',' for diagnostic control)");
+}
+
+TEST_F(ParserImplTest, DiagnosticControl_MissingRuleName) {
+    auto p = parser("(off,)");
+    auto e = p->expect_diagnostic_control();
+    EXPECT_TRUE(e.errored);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), R"(1:6: expected identifier for diagnostic control)");
+}
+
+TEST_F(ParserImplTest, DiagnosticControl_InvalidRuleName) {
+    auto p = parser("(off, foo$bar)");
+    auto e = p->expect_diagnostic_control();
+    EXPECT_TRUE(e.errored);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), R"(1:10: invalid character found)");
+}
+
+}  // namespace
+}  // namespace tint::reader::wgsl
diff --git a/src/tint/reader/wgsl/parser_impl_diagnostic_directive_test.cc b/src/tint/reader/wgsl/parser_impl_diagnostic_directive_test.cc
new file mode 100644
index 0000000..c91b7d1
--- /dev/null
+++ b/src/tint/reader/wgsl/parser_impl_diagnostic_directive_test.cc
@@ -0,0 +1,71 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/reader/wgsl/parser_impl_test_helper.h"
+
+#include "src/tint/ast/diagnostic_control.h"
+
+namespace tint::reader::wgsl {
+namespace {
+
+TEST_F(ParserImplTest, DiagnosticDirective_Valid) {
+    auto p = parser("diagnostic(off, foo);");
+    p->diagnostic_directive();
+    EXPECT_FALSE(p->has_error()) << p->error();
+    auto& ast = p->builder().AST();
+    ASSERT_EQ(ast.DiagnosticControls().Length(), 1u);
+    auto* control = ast.DiagnosticControls()[0];
+    EXPECT_EQ(control->severity, ast::DiagnosticSeverity::kOff);
+    ASSERT_EQ(ast.GlobalDeclarations().Length(), 1u);
+    EXPECT_EQ(ast.GlobalDeclarations()[0], control);
+
+    auto* r = As<ast::IdentifierExpression>(control->rule_name);
+    ASSERT_NE(r, nullptr);
+    EXPECT_EQ(p->builder().Symbols().NameFor(r->symbol), "foo");
+}
+
+TEST_F(ParserImplTest, DiagnosticDirective_MissingSemicolon) {
+    auto p = parser("diagnostic(off, foo)");
+    p->translation_unit();
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:21: expected ';' for diagnostic directive");
+    auto program = p->program();
+    auto& ast = program.AST();
+    EXPECT_EQ(ast.DiagnosticControls().Length(), 0u);
+    EXPECT_EQ(ast.GlobalDeclarations().Length(), 0u);
+}
+
+TEST_F(ParserImplTest, DiagnosticDirective_FollowingOtherGlobalDecl) {
+    auto p = parser(R"(
+var<private> t: f32 = 0f;
+diagnostic(off, foo);
+)");
+    p->translation_unit();
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "3:1: directives must come before all global declarations");
+}
+
+TEST_F(ParserImplTest, DiagnosticDirective_FollowingEmptySemicolon) {
+    auto p = parser(R"(
+;
+diagnostic(off, foo);
+)");
+    p->translation_unit();
+    // An empty semicolon is treated as a global declaration.
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "3:1: directives must come before all global declarations");
+}
+
+}  // namespace
+}  // namespace tint::reader::wgsl
diff --git a/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc b/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
index 392f0dd..23a8cb2 100644
--- a/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
@@ -161,7 +161,7 @@
 )");
     p->translation_unit();
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "3:1: enable directives must come before all global declarations");
+    EXPECT_EQ(p->error(), "3:1: directives must come before all global declarations");
     auto program = p->program();
     auto& ast = program.AST();
     // Accept the enable directive although it caused an error
@@ -181,7 +181,7 @@
     p->translation_unit();
     // An empty semicolon is treated as a global declaration
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "3:1: enable directives must come before all global declarations");
+    EXPECT_EQ(p->error(), "3:1: directives must come before all global declarations");
     auto program = p->program();
     auto& ast = program.AST();
     // Accept the enable directive although it cause an error
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 ff077ab..f458834 100644
--- a/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
@@ -52,8 +52,13 @@
 }
 
 TEST_F(ParserImplErrorTest, AliasDeclInvalidAttribute) {
-    EXPECT("@invariant type e=u32;",
-           R"(test.wgsl:1:2 error: unexpected attributes
+    EXPECT(
+        "@invariant type e=u32;",
+        R"(test.wgsl:1:12 warning: use of deprecated language feature: 'type' has been renamed to 'alias'
+@invariant type e=u32;
+           ^^^^
+
+test.wgsl:1:2 error: unexpected attributes
 @invariant type e=u32;
  ^^^^^^^^^
 )");
@@ -124,9 +129,9 @@
 
 TEST_F(ParserImplErrorTest, BitcastExprMissingGreaterThan) {
     EXPECT("fn f() { x = bitcast<u32(y); }",
-           R"(test.wgsl:1:25 error: expected '>' for bitcast expression
+           R"(test.wgsl:1:21 error: missing closing '>' for bitcast expression
 fn f() { x = bitcast<u32(y); }
-                        ^
+                    ^
 )");
 }
 
@@ -306,46 +311,130 @@
 )");
 }
 
-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, FunctionDeclConstAssertMissingCondThenEOF) {
+    EXPECT("fn f() { const_assert }", R"(test.wgsl:1:23 error: unable to parse condition expression
+fn f() { const_assert }
                       ^
 )");
 }
 
-TEST_F(ParserImplErrorTest, FunctionDeclStaticAssertMissingCondThenLet) {
-    EXPECT("fn f() { static_assert\nlet x = 0; }",
+TEST_F(ParserImplErrorTest, FunctionDeclConstAssertMissingCondThenSemicolon) {
+    EXPECT("fn f() { const_assert; }",
+           R"(test.wgsl:1:22 error: unable to parse condition expression
+fn f() { const_assert; }
+                     ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, FunctionDeclConstAssertMissingCondThenLet) {
+    EXPECT("fn f() { const_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
+TEST_F(ParserImplErrorTest, FunctionDeclConstAssertMissingLParen) {
+    EXPECT("fn f() { const_assert true);", R"(test.wgsl:1:27 error: expected ';' for statement
+fn f() { const_assert true);
+                          ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, FunctionDeclConstAssertMissingRParen) {
+    EXPECT("fn f() { const_assert (true;", R"(test.wgsl:1:28 error: expected ')'
+fn f() { const_assert (true;
+                           ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, FunctionDeclConstAssertMissingSemicolon) {
+    EXPECT("fn f() { const_assert true }",
+           R"(test.wgsl:1:28 error: expected ';' for statement
+fn f() { const_assert true }
+                           ^
+)");
+}
+
+// TODO(crbug.com/tint/1807)
+TEST_F(ParserImplErrorTest, DEPRECATED_FunctionDeclStaticAssertMissingCondThenEOF) {
+    EXPECT(
+        "fn f() { static_assert }",
+        R"(test.wgsl:1:10 warning: use of deprecated language feature: 'static_assert' has been renamed to 'const_assert'
+fn f() { static_assert }
+         ^^^^^^^^^^^^^
+
+test.wgsl:1:24 error: unable to parse condition expression
+fn f() { static_assert }
+                       ^
+)");
+}
+
+// TODO(crbug.com/tint/1807)
+TEST_F(ParserImplErrorTest, DEPRECATED_FunctionDeclStaticAssertMissingCondThenSemicolon) {
+    EXPECT(
+        "fn f() { static_assert; }",
+        R"(test.wgsl:1:10 warning: use of deprecated language feature: 'static_assert' has been renamed to 'const_assert'
+fn f() { static_assert; }
+         ^^^^^^^^^^^^^
+
+test.wgsl:1:23 error: unable to parse condition expression
+fn f() { static_assert; }
+                      ^
+)");
+}
+
+// TODO(crbug.com/tint/1807)
+TEST_F(ParserImplErrorTest, DEPRECATED_FunctionDeclStaticAssertMissingCondThenLet) {
+    EXPECT(
+        "fn f() { static_assert\nlet x = 0; }",
+        R"(test.wgsl:1:10 warning: use of deprecated language feature: 'static_assert' has been renamed to 'const_assert'
+fn f() { static_assert
+         ^^^^^^^^^^^^^
+
+test.wgsl:2:1 error: unable to parse condition expression
+let x = 0; }
+^^^
+)");
+}
+
+// TODO(crbug.com/tint/1807)
+TEST_F(ParserImplErrorTest, DEPRECATED_FunctionDeclStaticAssertMissingLParen) {
+    EXPECT(
+        "fn f() { static_assert true);",
+        R"(test.wgsl:1:10 warning: use of deprecated language feature: 'static_assert' has been renamed to 'const_assert'
+fn f() { static_assert true);
+         ^^^^^^^^^^^^^
+
+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 ')'
+// TODO(crbug.com/tint/1807)
+TEST_F(ParserImplErrorTest, DEPRECATED_FunctionDeclStaticAssertMissingRParen) {
+    EXPECT(
+        "fn f() { static_assert (true;",
+        R"(test.wgsl:1:10 warning: use of deprecated language feature: 'static_assert' has been renamed to 'const_assert'
+fn f() { static_assert (true;
+         ^^^^^^^^^^^^^
+
+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
+// TODO(crbug.com/tint/1807)
+TEST_F(ParserImplErrorTest, DEPRECATED_FunctionDeclStaticAssertMissingSemicolon) {
+    EXPECT(
+        "fn f() { static_assert true }",
+        R"(test.wgsl:1:10 warning: use of deprecated language feature: 'static_assert' has been renamed to 'const_assert'
+fn f() { static_assert true }
+         ^^^^^^^^^^^^^
+
+test.wgsl:1:29 error: expected ';' for statement
 fn f() { static_assert true }
                             ^
 )");
@@ -579,9 +668,9 @@
 
 TEST_F(ParserImplErrorTest, GlobalDeclSampledTextureMissingGreaterThan) {
     EXPECT("var x : texture_1d<f32;",
-           R"(test.wgsl:1:23 error: expected '>' for sampled texture type
+           R"(test.wgsl:1:19 error: missing closing '>' for sampled texture type
 var x : texture_1d<f32;
-                      ^
+                  ^
 )");
 }
 
@@ -603,9 +692,9 @@
 
 TEST_F(ParserImplErrorTest, GlobalDeclMultisampledTextureMissingGreaterThan) {
     EXPECT("var x : texture_multisampled_2d<f32;",
-           R"(test.wgsl:1:36 error: expected '>' for multisampled texture type
+           R"(test.wgsl:1:32 error: missing closing '>' for multisampled texture type
 var x : texture_multisampled_2d<f32;
-                                   ^
+                               ^
 )");
 }
 
@@ -617,46 +706,124 @@
 )");
 }
 
-TEST_F(ParserImplErrorTest, GlobalDeclStaticAssertMissingCondThenEOF) {
-    EXPECT("static_assert", R"(test.wgsl:1:14 error: unable to parse condition expression
-static_assert
-             ^
+TEST_F(ParserImplErrorTest, GlobalDeclConstAssertMissingCondThenEOF) {
+    EXPECT("const_assert", R"(test.wgsl:1:13 error: unable to parse condition expression
+const_assert
+            ^
 )");
 }
 
-TEST_F(ParserImplErrorTest, GlobalDeclStaticAssertMissingCondThenSemicolon) {
-    EXPECT("static_assert;", R"(test.wgsl:1:14 error: unable to parse condition expression
-static_assert;
-             ^
+TEST_F(ParserImplErrorTest, GlobalDeclConstAssertMissingCondThenSemicolon) {
+    EXPECT("const_assert;", R"(test.wgsl:1:13 error: unable to parse condition expression
+const_assert;
+            ^
 )");
 }
 
-TEST_F(ParserImplErrorTest, GlobalDeclStaticAssertMissingCondThenAlias) {
-    EXPECT("static_assert\ntype T = i32;",
+TEST_F(ParserImplErrorTest, GlobalDeclConstAssertMissingCondThenAlias) {
+    EXPECT("const_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
+TEST_F(ParserImplErrorTest, GlobalDeclConstAssertMissingLParen) {
+    EXPECT("const_assert true);",
+           R"(test.wgsl:1:18 error: expected ';' for const assertion declaration
+const_assert true);
+                 ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclConstAssertMissingRParen) {
+    EXPECT("const_assert (true;", R"(test.wgsl:1:19 error: expected ')'
+const_assert (true;
+                  ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclConstAssertMissingSemicolon) {
+    EXPECT("const_assert true const_assert true;",
+           R"(test.wgsl:1:19 error: expected ';' for const assertion declaration
+const_assert true const_assert true;
+                  ^^^^^^^^^^^^
+)");
+}
+
+// TODO(crbug.com/tint/1807): DEPRECATED
+TEST_F(ParserImplErrorTest, DEPRECATED_GlobalDeclStaticAssertMissingCondThenEOF) {
+    EXPECT("const_assert", R"(test.wgsl:1:13 error: unable to parse condition expression
+const_assert
+            ^
+)");
+}
+
+// TODO(crbug.com/tint/1807): DEPRECATED
+TEST_F(ParserImplErrorTest, DEPRECATED_GlobalDeclStaticAssertMissingCondThenSemicolon) {
+    EXPECT(
+        "static_assert;",
+        R"(test.wgsl:1:1 warning: use of deprecated language feature: 'static_assert' has been renamed to 'const_assert'
+static_assert;
+^^^^^^^^^^^^^
+
+test.wgsl:1:14 error: unable to parse condition expression
+static_assert;
+             ^
+)");
+}
+
+// TODO(crbug.com/tint/1807): DEPRECATED
+TEST_F(ParserImplErrorTest, DEPRECATED_GlobalDeclStaticAssertMissingCondThenAlias) {
+    EXPECT(
+        "static_assert\ntype T = i32;",
+        R"(test.wgsl:1:1 warning: use of deprecated language feature: 'static_assert' has been renamed to 'const_assert'
+static_assert
+^^^^^^^^^^^^^
+
+test.wgsl:2:1 error: unable to parse condition expression
+type T = i32;
+^^^^
+)");
+}
+
+// TODO(crbug.com/tint/1807): DEPRECATED
+TEST_F(ParserImplErrorTest, DEPRECATED_GlobalDeclStaticAssertMissingLParen) {
+    EXPECT(
+        "static_assert true);",
+        R"(test.wgsl:1:1 warning: use of deprecated language feature: 'static_assert' has been renamed to 'const_assert'
+static_assert true);
+^^^^^^^^^^^^^
+
+test.wgsl:1:19 error: expected ';' for const assertion declaration
 static_assert true);
                   ^
 )");
 }
 
-TEST_F(ParserImplErrorTest, GlobalDeclStaticAssertMissingRParen) {
-    EXPECT("static_assert (true;", R"(test.wgsl:1:20 error: expected ')'
+// TODO(crbug.com/tint/1807): DEPRECATED
+TEST_F(ParserImplErrorTest, DEPRECATED_GlobalDeclStaticAssertMissingRParen) {
+    EXPECT(
+        "static_assert (true;",
+        R"(test.wgsl:1:1 warning: use of deprecated language feature: 'static_assert' has been renamed to 'const_assert'
+static_assert (true;
+^^^^^^^^^^^^^
+
+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
+// TODO(crbug.com/tint/1807): DEPRECATED
+TEST_F(ParserImplErrorTest, DEPRECATED_GlobalDeclStaticAssertMissingSemicolon) {
+    EXPECT(
+        "static_assert true static_assert true;",
+        R"(test.wgsl:1:1 warning: use of deprecated language feature: 'static_assert' has been renamed to 'const_assert'
+static_assert true static_assert true;
+^^^^^^^^^^^^^
+
+test.wgsl:1:20 error: expected ';' for const assertion declaration
 static_assert true static_assert true;
                    ^^^^^^^^^^^^^
 )");
@@ -672,9 +839,9 @@
 
 TEST_F(ParserImplErrorTest, GlobalDeclStorageTextureMissingGreaterThan) {
     EXPECT("var x : texture_storage_2d<r32uint, read;",
-           R"(test.wgsl:1:41 error: expected '>' for storage texture type
+           R"(test.wgsl:1:27 error: missing closing '>' for storage texture type
 var x : texture_storage_2d<r32uint, read;
-                                        ^
+                          ^
 )");
 }
 
@@ -745,29 +912,80 @@
 }
 
 TEST_F(ParserImplErrorTest, GlobalDeclTypeAliasMissingIdentifier) {
-    EXPECT("type 1 = f32;",
-           R"(test.wgsl:1:6 error: expected identifier for type alias
-type 1 = f32;
-     ^
+    EXPECT("alias 1 = f32;",
+           R"(test.wgsl:1:7 error: expected identifier for type alias
+alias 1 = f32;
+      ^
 )");
 }
 
 TEST_F(ParserImplErrorTest, GlobalDeclTypeAliasInvalidType) {
-    EXPECT("type meow = 1;", R"(test.wgsl:1:13 error: invalid type alias
+    EXPECT("alias meow = 1;", R"(test.wgsl:1:14 error: invalid type alias
+alias meow = 1;
+             ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclTypeAliasMissingAssignment) {
+    EXPECT("alias meow f32", R"(test.wgsl:1:12 error: expected '=' for type alias
+alias meow f32
+           ^^^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclTypeAliasMissingSemicolon) {
+    EXPECT("alias meow = f32", R"(test.wgsl:1:17 error: expected ';' for type alias
+alias meow = f32
+                ^
+)");
+}
+
+// TODO(crbug.com/tint/1812): DEPRECATED
+TEST_F(ParserImplErrorTest, DEPRECATED_GlobalDeclTypeAliasMissingIdentifier) {
+    EXPECT("alias 1 = f32;",
+           R"(test.wgsl:1:7 error: expected identifier for type alias
+alias 1 = f32;
+      ^
+)");
+}
+
+// TODO(crbug.com/tint/1812): DEPRECATED
+TEST_F(ParserImplErrorTest, DEPRECATED_GlobalDeclTypeAliasInvalidType) {
+    EXPECT(
+        "type meow = 1;",
+        R"(test.wgsl:1:1 warning: use of deprecated language feature: 'type' has been renamed to 'alias'
+type meow = 1;
+^^^^
+
+test.wgsl:1:13 error: invalid type alias
 type meow = 1;
             ^
 )");
 }
 
-TEST_F(ParserImplErrorTest, GlobalDeclTypeAliasMissingAssignment) {
-    EXPECT("type meow f32", R"(test.wgsl:1:11 error: expected '=' for type alias
+// TODO(crbug.com/tint/1812): DEPRECATED
+TEST_F(ParserImplErrorTest, DEPRECATED_GlobalDeclTypeAliasMissingAssignment) {
+    EXPECT(
+        "type meow f32",
+        R"(test.wgsl:1:1 warning: use of deprecated language feature: 'type' has been renamed to 'alias'
+type meow f32
+^^^^
+
+test.wgsl:1:11 error: expected '=' for type alias
 type meow f32
           ^^^
 )");
 }
 
-TEST_F(ParserImplErrorTest, GlobalDeclTypeAliasMissingSemicolon) {
-    EXPECT("type meow = f32", R"(test.wgsl:1:16 error: expected ';' for type alias
+// TODO(crbug.com/tint/1812): DEPRECATED
+TEST_F(ParserImplErrorTest, DEPRECATED_GlobalDeclTypeAliasMissingSemicolon) {
+    EXPECT(
+        "type meow = f32",
+        R"(test.wgsl:1:1 warning: use of deprecated language feature: 'type' has been renamed to 'alias'
+type meow = f32
+^^^^
+
+test.wgsl:1:16 error: expected ';' for type alias
 type meow = f32
                ^
 )");
@@ -775,9 +993,9 @@
 
 TEST_F(ParserImplErrorTest, GlobalDeclVarArrayMissingGreaterThan) {
     EXPECT("var i : array<u32, 3;",
-           R"(test.wgsl:1:21 error: expected '>' for array declaration
+           R"(test.wgsl:1:14 error: missing closing '>' for array declaration
 var i : array<u32, 3;
-                    ^
+             ^
 )");
 }
 
@@ -956,9 +1174,9 @@
 }
 
 TEST_F(ParserImplErrorTest, GlobalDeclVarMatrixMissingGreaterThan) {
-    EXPECT("var i : mat4x4<u32;", R"(test.wgsl:1:19 error: expected '>' for matrix
+    EXPECT("var i : mat4x4<u32;", R"(test.wgsl:1:15 error: missing closing '>' for matrix
 var i : mat4x4<u32;
-                  ^
+              ^
 )");
 }
 
@@ -987,9 +1205,9 @@
 
 TEST_F(ParserImplErrorTest, GlobalDeclVarPtrMissingGreaterThan) {
     EXPECT("var i : ptr<private, u32;",
-           R"(test.wgsl:1:25 error: expected '>' for ptr declaration
+           R"(test.wgsl:1:12 error: missing closing '>' for ptr declaration
 var i : ptr<private, u32;
-                        ^
+           ^
 )");
 }
 
@@ -1028,9 +1246,9 @@
 
 TEST_F(ParserImplErrorTest, GlobalDeclVarAtomicMissingGreaterThan) {
     EXPECT("var i : atomic<u32 x;",
-           R"(test.wgsl:1:20 error: expected '>' for atomic declaration
+           R"(test.wgsl:1:15 error: missing closing '>' for atomic declaration
 var i : atomic<u32 x;
-                   ^
+              ^
 )");
 }
 
@@ -1052,9 +1270,9 @@
 }
 
 TEST_F(ParserImplErrorTest, GlobalDeclVarVectorMissingGreaterThan) {
-    EXPECT("var i : vec3<u32;", R"(test.wgsl:1:17 error: expected '>' for vector
+    EXPECT("var i : vec3<u32;", R"(test.wgsl:1:13 error: missing closing '>' for vector
 var i : vec3<u32;
-                ^
+            ^
 )");
 }
 
diff --git a/src/tint/reader/wgsl/parser_impl_expression_test.cc b/src/tint/reader/wgsl/parser_impl_expression_test.cc
index 71d1d99..9ae4591 100644
--- a/src/tint/reader/wgsl/parser_impl_expression_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_expression_test.cc
@@ -463,6 +463,9 @@
     bool should_parse;
 };
 
+static bool ParsedAsTemplateArgumentList(BinaryOperatorInfo lhs_op, BinaryOperatorInfo rhs_op) {
+    return lhs_op.bit == kOpLt && rhs_op.bit & (kOpGt | kOpGe | kOpShr);
+}
 static std::ostream& operator<<(std::ostream& o, const Case& c) {
     return o << "a " << c.lhs_op.symbol << " b " << c.rhs_op.symbol << " c ";
 }
@@ -471,7 +474,8 @@
     std::vector<Case> out;
     for (auto& lhs_op : kBinaryOperators) {
         for (auto& rhs_op : kBinaryOperators) {
-            bool should_parse = lhs_op.can_follow_without_paren & rhs_op.bit;
+            bool should_parse = (lhs_op.can_follow_without_paren & rhs_op.bit) &&
+                                !ParsedAsTemplateArgumentList(lhs_op, rhs_op);
             out.push_back({lhs_op, rhs_op, should_parse});
         }
     }
@@ -494,8 +498,14 @@
         EXPECT_EQ(e.value, nullptr);
         EXPECT_TRUE(p->has_error());
         std::stringstream expected;
-        expected << "1:3: mixing '" << GetParam().lhs_op.symbol << "' and '"
-                 << GetParam().rhs_op.symbol << "' requires parenthesis";
+        if (ParsedAsTemplateArgumentList(GetParam().lhs_op, GetParam().rhs_op)) {
+            expected << "1:3: '<' treated as the start of a template argument list, which is not "
+                        "supported for user-declared types or functions. If you intended "
+                        "less-than, wrap the expression in parentheses";
+        } else {
+            expected << "1:3: mixing '" << GetParam().lhs_op.symbol << "' and '"
+                     << GetParam().rhs_op.symbol << "' requires parenthesis";
+        }
         EXPECT_EQ(p->error(), expected.str());
     }
 }
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 2f124d5..c79af0d 100644
--- a/src/tint/reader/wgsl/parser_impl_global_decl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_global_decl_test.cc
@@ -99,7 +99,7 @@
 }
 
 TEST_F(ParserImplTest, GlobalDecl_TypeAlias) {
-    auto p = parser("type A = i32;");
+    auto p = parser("alias A = i32;");
     p->global_decl();
     ASSERT_FALSE(p->has_error()) << p->error();
 
@@ -113,6 +113,42 @@
     auto p = parser(R"(struct A {
   a : f32,
 }
+alias B = A;)");
+    p->global_decl();
+    p->global_decl();
+    ASSERT_FALSE(p->has_error()) << p->error();
+
+    auto program = p->program();
+    ASSERT_EQ(program.AST().TypeDecls().Length(), 2u);
+    ASSERT_TRUE(program.AST().TypeDecls()[0]->Is<ast::Struct>());
+    auto* str = program.AST().TypeDecls()[0]->As<ast::Struct>();
+    EXPECT_EQ(str->name, program.Symbols().Get("A"));
+
+    ASSERT_TRUE(program.AST().TypeDecls()[1]->Is<ast::Alias>());
+    auto* alias = program.AST().TypeDecls()[1]->As<ast::Alias>();
+    EXPECT_EQ(alias->name, program.Symbols().Get("B"));
+    auto* tn = alias->type->As<ast::TypeName>();
+    EXPECT_NE(tn, nullptr);
+    EXPECT_EQ(tn->name, str->name);
+}
+
+// TODO(crbug.com/tint/1812): DEPRECATED
+TEST_F(ParserImplTest, DEPRECATED_GlobalDecl_TypeAlias) {
+    auto p = parser("type A = i32;");
+    p->global_decl();
+    ASSERT_FALSE(p->has_error()) << p->error();
+
+    auto program = p->program();
+    ASSERT_EQ(program.AST().TypeDecls().Length(), 1u);
+    ASSERT_TRUE(program.AST().TypeDecls()[0]->Is<ast::Alias>());
+    EXPECT_EQ(program.Symbols().NameFor(program.AST().TypeDecls()[0]->As<ast::Alias>()->name), "A");
+}
+
+// TODO(crbug.com/tint/1812): DEPRECATED
+TEST_F(ParserImplTest, DEPRECATED_GlobalDecl_TypeAlias_StructIdent) {
+    auto p = parser(R"(struct A {
+  a : f32,
+}
 type B = A;)");
     p->global_decl();
     p->global_decl();
@@ -133,10 +169,20 @@
 }
 
 TEST_F(ParserImplTest, GlobalDecl_TypeAlias_MissingSemicolon) {
+    auto p = parser("alias A = i32");
+    p->global_decl();
+    ASSERT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:14: expected ';' for type alias");
+}
+
+// TODO(crbug.com/tint/1812): DEPRECATED
+TEST_F(ParserImplTest, DEPRECATED_GlobalDecl_TypeAlias_MissingSemicolon) {
     auto p = parser("type A = i32");
     p->global_decl();
     ASSERT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:13: expected ';' for type alias");
+    EXPECT_EQ(p->error(),
+              R"(1:1: use of deprecated language feature: 'type' has been renamed to 'alias'
+1:13: expected ';' for type alias)");
 }
 
 TEST_F(ParserImplTest, GlobalDecl_Function) {
@@ -211,14 +257,57 @@
     EXPECT_EQ(p->error(), "1:2: unexpected attributes");
 }
 
-TEST_F(ParserImplTest, GlobalDecl_StaticAssert_WithParen) {
+TEST_F(ParserImplTest, GlobalDecl_ConstAssert_WithParen) {
+    auto p = parser("const_assert(true);");
+    p->global_decl();
+    ASSERT_FALSE(p->has_error()) << p->error();
+
+    auto program = p->program();
+    ASSERT_EQ(program.AST().ConstAsserts().Length(), 1u);
+    auto* sa = program.AST().ConstAsserts()[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, 19u);
+
+    EXPECT_TRUE(sa->condition->Is<ast::BoolLiteralExpression>());
+    EXPECT_EQ(sa->condition->source.range.begin.line, 1u);
+    EXPECT_EQ(sa->condition->source.range.begin.column, 14u);
+    EXPECT_EQ(sa->condition->source.range.end.line, 1u);
+    EXPECT_EQ(sa->condition->source.range.end.column, 18u);
+}
+
+TEST_F(ParserImplTest, GlobalDecl_ConstAssert_WithoutParen) {
+    auto p = parser("const_assert  true;");
+    p->global_decl();
+    ASSERT_FALSE(p->has_error()) << p->error();
+
+    auto program = p->program();
+    ASSERT_EQ(program.AST().ConstAsserts().Length(), 1u);
+    auto* sa = program.AST().ConstAsserts()[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, 19u);
+
+    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);
+}
+
+// TODO(crbug.com/tint/1807)
+TEST_F(ParserImplTest, DEPRECATED_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];
+    ASSERT_EQ(program.AST().ConstAsserts().Length(), 1u);
+    auto* sa = program.AST().ConstAsserts()[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);
@@ -231,14 +320,15 @@
     EXPECT_EQ(sa->condition->source.range.end.column, 19u);
 }
 
-TEST_F(ParserImplTest, GlobalDecl_StaticAssert_WithoutParen) {
+// TODO(crbug.com/tint/1807)
+TEST_F(ParserImplTest, DEPRECATED_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];
+    ASSERT_EQ(program.AST().ConstAsserts().Length(), 1u);
+    auto* sa = program.AST().ConstAsserts()[0];
     EXPECT_TRUE(sa->condition->Is<ast::BoolLiteralExpression>());
 
     EXPECT_EQ(sa->source.range.begin.line, 1u);
diff --git a/src/tint/reader/wgsl/parser_impl_primary_expression_test.cc b/src/tint/reader/wgsl/parser_impl_primary_expression_test.cc
index f7f9a87..ec6561f 100644
--- a/src/tint/reader/wgsl/parser_impl_primary_expression_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_primary_expression_test.cc
@@ -266,7 +266,7 @@
     EXPECT_TRUE(e.errored);
     EXPECT_EQ(e.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:12: expected '>' for bitcast expression");
+    EXPECT_EQ(p->error(), "1:8: missing closing '>' for bitcast expression");
 }
 
 TEST_F(ParserImplTest, PrimaryExpression_Bitcast_MissingType) {
@@ -319,5 +319,18 @@
     EXPECT_EQ(p->error(), "1:14: unable to parse expression");
 }
 
+TEST_F(ParserImplTest, PrimaryExpression_Template) {
+    auto p = parser("a<b>()");
+    auto e = p->primary_expression();
+    EXPECT_FALSE(e.matched);
+    EXPECT_TRUE(e.errored);
+    EXPECT_EQ(e.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(),
+              "1:2: '<' treated as the start of a template argument list, which is not supported "
+              "for user-declared types or functions. If you intended less-than, wrap the "
+              "expression in parentheses");
+}
+
 }  // 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 f2adc06..c20302d 100644
--- a/src/tint/reader/wgsl/parser_impl_reserved_keyword_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_reserved_keyword_test.cc
@@ -78,10 +78,10 @@
 }
 TEST_P(ParserImplReservedKeywordTest, Alias) {
     auto name = GetParam();
-    auto p = parser("type " + name + " = i32;");
+    auto p = parser("alias " + name + " = i32;");
     EXPECT_FALSE(p->Parse());
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:6: '" + name + "' is a reserved keyword");
+    EXPECT_EQ(p->error(), "1:7: '" + name + "' is a reserved keyword");
 }
 INSTANTIATE_TEST_SUITE_P(ParserImplReservedKeywordTest,
                          ParserImplReservedKeywordTest,
diff --git a/src/tint/reader/wgsl/parser_impl_statement_test.cc b/src/tint/reader/wgsl/parser_impl_statement_test.cc
index f78e944..f21a4e7 100644
--- a/src/tint/reader/wgsl/parser_impl_statement_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_statement_test.cc
@@ -272,14 +272,57 @@
     EXPECT_EQ(p->error(), "1:3: expected '}'");
 }
 
-TEST_F(ParserImplTest, Statement_StaticAssert_WithParen) {
+TEST_F(ParserImplTest, Statement_ConstAssert_WithParen) {
+    auto p = parser("const_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::ConstAssert>(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, 19u);
+
+    EXPECT_TRUE(sa->condition->Is<ast::BoolLiteralExpression>());
+    EXPECT_EQ(sa->condition->source.range.begin.line, 1u);
+    EXPECT_EQ(sa->condition->source.range.begin.column, 14u);
+    EXPECT_EQ(sa->condition->source.range.end.line, 1u);
+    EXPECT_EQ(sa->condition->source.range.end.column, 18u);
+}
+
+TEST_F(ParserImplTest, Statement_ConstAssert_WithoutParen) {
+    auto p = parser("const_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::ConstAssert>(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, 19u);
+
+    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);
+}
+
+// TODO(crbug.com/tint/1807)
+TEST_F(ParserImplTest, DEPRECATED_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);
+    auto* sa = As<ast::ConstAssert>(e.value);
     ASSERT_NE(sa, nullptr);
     EXPECT_EQ(sa->source.range.begin.line, 1u);
     EXPECT_EQ(sa->source.range.begin.column, 1u);
@@ -293,14 +336,15 @@
     EXPECT_EQ(sa->condition->source.range.end.column, 19u);
 }
 
-TEST_F(ParserImplTest, Statement_StaticAssert_WithoutParen) {
+// TODO(crbug.com/tint/1807)
+TEST_F(ParserImplTest, DEPRECATED_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);
+    auto* sa = As<ast::ConstAssert>(e.value);
     ASSERT_NE(sa, nullptr);
     EXPECT_EQ(sa->source.range.begin.line, 1u);
     EXPECT_EQ(sa->source.range.begin.column, 1u);
diff --git a/src/tint/reader/wgsl/parser_impl_test.cc b/src/tint/reader/wgsl/parser_impl_test.cc
index 21efd2f..6df73c4 100644
--- a/src/tint/reader/wgsl/parser_impl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_test.cc
@@ -154,7 +154,7 @@
     auto& n = p->next();
     ASSERT_TRUE(n.Is(Token::Type::kGreaterThanEqual));
     EXPECT_TRUE(p->peek_is(Token::Type::kVec2)) << "expected: vec2 got: " << p->peek().to_name();
-    EXPECT_TRUE(p->peek_is(Token::Type::kLessThan, 1))
+    EXPECT_TRUE(p->peek_is(Token::Type::kTemplateArgsLeft, 1))
         << "expected: < got: " << p->peek(1).to_name();
 }
 
@@ -163,10 +163,10 @@
     auto& n = p->next();
     ASSERT_TRUE(n.Is(Token::Type::kGreaterThanEqual));
     EXPECT_TRUE(p->peek_is(Token::Type::kGreaterThanEqual))
-        << "expected: <= got: " << p->peek().to_name();
+        << "expected: >= got: " << p->peek().to_name();
     EXPECT_TRUE(p->peek_is(Token::Type::kVec2, 1))
         << "expected: vec2 got: " << p->peek(1).to_name();
-    EXPECT_TRUE(p->peek_is(Token::Type::kLessThan, 2))
+    EXPECT_TRUE(p->peek_is(Token::Type::kTemplateArgsLeft, 2))
         << "expected: < got: " << p->peek(2).to_name();
 }
 
diff --git a/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc b/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc
index 43d4ada..299bd33 100644
--- a/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc
@@ -136,7 +136,7 @@
     EXPECT_EQ(t.value, nullptr);
     EXPECT_FALSE(t.matched);
     EXPECT_TRUE(t.errored);
-    EXPECT_EQ(p->error(), "1:15: expected '>' for sampled texture type");
+    EXPECT_EQ(p->error(), "1:11: missing closing '>' for sampled texture type");
 }
 
 TEST_F(ParserImplTest, TextureSamplerTypes_MultisampledTexture_I32) {
@@ -178,7 +178,7 @@
     EXPECT_EQ(t.value, nullptr);
     EXPECT_FALSE(t.matched);
     EXPECT_TRUE(t.errored);
-    EXPECT_EQ(p->error(), "1:28: expected '>' for multisampled texture type");
+    EXPECT_EQ(p->error(), "1:24: missing closing '>' for multisampled texture type");
 }
 
 TEST_F(ParserImplTest, TextureSamplerTypes_StorageTexture_Readonly1dRg32Float) {
@@ -270,7 +270,7 @@
     EXPECT_EQ(t.value, nullptr);
     EXPECT_FALSE(t.matched);
     EXPECT_TRUE(t.errored);
-    EXPECT_EQ(p->error(), "1:33: expected '>' for storage texture type");
+    EXPECT_EQ(p->error(), "1:19: missing closing '>' for storage texture type");
 }
 
 }  // namespace
diff --git a/src/tint/reader/wgsl/parser_impl_type_alias_test.cc b/src/tint/reader/wgsl/parser_impl_type_alias_test.cc
index c9bb72c..f4c20de 100644
--- a/src/tint/reader/wgsl/parser_impl_type_alias_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_type_alias_test.cc
@@ -73,7 +73,9 @@
     EXPECT_FALSE(t.matched);
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(t.value, nullptr);
-    EXPECT_EQ(p->error(), "1:6: expected identifier for type alias");
+    EXPECT_EQ(p->error(),
+              R"(1:1: use of deprecated language feature: 'type' has been renamed to 'alias'
+1:6: expected identifier for type alias)");
 }
 
 TEST_F(ParserImplTest, TypeDecl_InvalidIdent) {
@@ -83,7 +85,9 @@
     EXPECT_FALSE(t.matched);
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(t.value, nullptr);
-    EXPECT_EQ(p->error(), "1:6: expected identifier for type alias");
+    EXPECT_EQ(p->error(),
+              R"(1:1: use of deprecated language feature: 'type' has been renamed to 'alias'
+1:6: expected identifier for type alias)");
 }
 
 TEST_F(ParserImplTest, TypeDecl_MissingEqual) {
@@ -93,7 +97,9 @@
     EXPECT_FALSE(t.matched);
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(t.value, nullptr);
-    EXPECT_EQ(p->error(), "1:8: expected '=' for type alias");
+    EXPECT_EQ(p->error(),
+              R"(1:1: use of deprecated language feature: 'type' has been renamed to 'alias'
+1:8: expected '=' for type alias)");
 }
 
 }  // namespace
diff --git a/src/tint/reader/wgsl/parser_impl_type_decl_test.cc b/src/tint/reader/wgsl/parser_impl_type_decl_test.cc
index 1d54255..5e18b00 100644
--- a/src/tint/reader/wgsl/parser_impl_type_decl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_type_decl_test.cc
@@ -139,7 +139,7 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:9: expected '>' for vector");
+    ASSERT_EQ(p->error(), "1:5: missing closing '>' for vector");
 }
 INSTANTIATE_TEST_SUITE_P(ParserImplTest,
                          VecMissingGreaterThanTest,
@@ -232,7 +232,7 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:18: expected '>' for ptr declaration");
+    ASSERT_EQ(p->error(), "1:4: missing closing '>' for ptr declaration");
 }
 
 TEST_F(ParserImplTest, TypeDecl_Ptr_MissingGreaterThanAfterAccess) {
@@ -242,7 +242,7 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:24: expected '>' for ptr declaration");
+    ASSERT_EQ(p->error(), "1:4: missing closing '>' for ptr declaration");
 }
 
 TEST_F(ParserImplTest, TypeDecl_Ptr_MissingCommaAfterAddressSpace) {
@@ -380,7 +380,7 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:11: expected '>' for atomic declaration");
+    ASSERT_EQ(p->error(), "1:7: missing closing '>' for atomic declaration");
 }
 
 TEST_F(ParserImplTest, TypeDecl_Atomic_MissingType) {
@@ -561,7 +561,7 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:10: expected '>' for array declaration");
+    ASSERT_EQ(p->error(), "1:6: missing closing '>' for array declaration");
 }
 
 TEST_F(ParserImplTest, TypeDecl_Array_MissingComma) {
@@ -623,7 +623,7 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:11: expected '>' for matrix");
+    ASSERT_EQ(p->error(), "1:7: missing closing '>' for matrix");
 }
 INSTANTIATE_TEST_SUITE_P(ParserImplTest,
                          MatrixMissingGreaterThanTest,
diff --git a/src/tint/reader/wgsl/parser_impl_type_decl_without_ident_test.cc b/src/tint/reader/wgsl/parser_impl_type_decl_without_ident_test.cc
index f2ab172..ea146c9 100644
--- a/src/tint/reader/wgsl/parser_impl_type_decl_without_ident_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_type_decl_without_ident_test.cc
@@ -130,7 +130,7 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:9: expected '>' for vector");
+    ASSERT_EQ(p->error(), "1:5: missing closing '>' for vector");
 }
 INSTANTIATE_TEST_SUITE_P(ParserImplTest,
                          TypeDeclWithoutIdent_VecMissingGreaterThanTest,
@@ -223,7 +223,7 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:18: expected '>' for ptr declaration");
+    ASSERT_EQ(p->error(), "1:4: missing closing '>' for ptr declaration");
 }
 
 TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_MissingGreaterThanAfterAccess) {
@@ -233,7 +233,7 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:24: expected '>' for ptr declaration");
+    ASSERT_EQ(p->error(), "1:4: missing closing '>' for ptr declaration");
 }
 
 TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_MissingCommaAfterAddressSpace) {
@@ -371,7 +371,7 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:11: expected '>' for atomic declaration");
+    ASSERT_EQ(p->error(), "1:7: missing closing '>' for atomic declaration");
 }
 
 TEST_F(ParserImplTest, TypeDeclWithoutIdent_Atomic_MissingType) {
@@ -552,7 +552,7 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:10: expected '>' for array declaration");
+    ASSERT_EQ(p->error(), "1:6: missing closing '>' for array declaration");
 }
 
 TEST_F(ParserImplTest, TypeDeclWithoutIdent_Array_MissingComma) {
@@ -615,7 +615,7 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:11: expected '>' for matrix");
+    ASSERT_EQ(p->error(), "1:7: missing closing '>' for matrix");
 }
 INSTANTIATE_TEST_SUITE_P(ParserImplTest,
                          TypeDeclWithoutIdent_MatrixMissingGreaterThanTest,
diff --git a/src/tint/reader/wgsl/token.cc b/src/tint/reader/wgsl/token.cc
index 8e3fbdb..f13b317 100644
--- a/src/tint/reader/wgsl/token.cc
+++ b/src/tint/reader/wgsl/token.cc
@@ -70,12 +70,14 @@
             return "=";
         case Token::Type::kEqualEqual:
             return "==";
+        case Token::Type::kTemplateArgsRight:
         case Token::Type::kGreaterThan:
             return ">";
         case Token::Type::kGreaterThanEqual:
             return ">=";
         case Token::Type::kShiftRight:
             return ">>";
+        case Token::Type::kTemplateArgsLeft:
         case Token::Type::kLessThan:
             return "<";
         case Token::Type::kLessThanEqual:
@@ -135,6 +137,8 @@
         case Token::Type::kShiftRightEqual:
             return ">>=";
 
+        case Token::Type::kAlias:
+            return "alias";
         case Token::Type::kArray:
             return "array";
         case Token::Type::kAtomic:
@@ -149,6 +153,8 @@
             return "case";
         case Token::Type::kConst:
             return "const";
+        case Token::Type::kConstAssert:
+            return "const_assert";
         case Token::Type::kContinue:
             return "continue";
         case Token::Type::kContinuing:
diff --git a/src/tint/reader/wgsl/token.h b/src/tint/reader/wgsl/token.h
index 1c8531b..7935cc3 100644
--- a/src/tint/reader/wgsl/token.h
+++ b/src/tint/reader/wgsl/token.h
@@ -32,7 +32,7 @@
         kError = -2,
         /// Uninitialized token
         kUninitialized = 0,
-        /// Placeholder token which maybe fillled in later
+        /// Placeholder token which maybe filled in later
         kPlaceholder = 1,
         /// End of input string reached
         kEOF,
@@ -80,12 +80,16 @@
         kEqual,
         /// A '=='
         kEqualEqual,
+        /// A '>' (post template-args classification)
+        kTemplateArgsRight,
         /// A '>'
         kGreaterThan,
         /// A '>='
         kGreaterThanEqual,
         /// A '>>'
         kShiftRight,
+        /// A '<' (post template-args classification)
+        kTemplateArgsLeft,
         /// A '<'
         kLessThan,
         /// A '<='
@@ -145,6 +149,8 @@
         /// A '<<='
         kShiftLeftEqual,
 
+        /// A 'alias'
+        kAlias,
         /// A 'array'
         kArray,
         /// A 'atomic'
@@ -159,6 +165,8 @@
         kCase,
         /// A 'const'
         kConst,
+        /// A 'const_assert'
+        kConstAssert,
         /// A 'continue'
         kContinue,
         /// A 'continuing'
@@ -452,12 +460,10 @@
     std::variant<int64_t, double, std::string, std::string_view> value_;
 };
 
-#ifndef NDEBUG
 inline std::ostream& operator<<(std::ostream& out, Token::Type type) {
     out << Token::TypeToName(type);
     return out;
 }
-#endif  // NDEBUG
 
 }  // namespace tint::reader::wgsl
 
diff --git a/src/tint/resolver/alias_analysis_test.cc b/src/tint/resolver/alias_analysis_test.cc
index db78d64..66d5033 100644
--- a/src/tint/resolver/alias_analysis_test.cc
+++ b/src/tint/resolver/alias_analysis_test.cc
@@ -36,7 +36,7 @@
 // }
 struct TwoPointerConfig {
     type::AddressSpace address_space;  // The address space for the pointers.
-    bool aliased;                     // Whether the pointers alias or not.
+    bool aliased;                      // Whether the pointers alias or not.
 };
 class TwoPointers : public ResolverTestWithParam<TwoPointerConfig> {
   protected:
diff --git a/src/tint/resolver/const_assert_test.cc b/src/tint/resolver/const_assert_test.cc
new file mode 100644
index 0000000..fb08b8b
--- /dev/null
+++ b/src/tint/resolver/const_assert_test.cc
@@ -0,0 +1,107 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/resolver/resolver.h"
+
+#include "gmock/gmock.h"
+#include "src/tint/resolver/resolver_test_helper.h"
+
+using namespace tint::number_suffixes;  // NOLINT
+
+namespace tint::resolver {
+namespace {
+
+using ResolverConstAssertTest = ResolverTest;
+
+TEST_F(ResolverConstAssertTest, Global_True_Pass) {
+    GlobalConstAssert(true);
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverConstAssertTest, Global_False_Fail) {
+    GlobalConstAssert(Source{{12, 34}}, false);
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: const assertion failed");
+}
+
+TEST_F(ResolverConstAssertTest, Global_Const_Pass) {
+    GlobalConst("C", ty.bool_(), Expr(true));
+    GlobalConstAssert("C");
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverConstAssertTest, Global_Const_Fail) {
+    GlobalConst("C", ty.bool_(), Expr(false));
+    GlobalConstAssert(Source{{12, 34}}, "C");
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: const assertion failed");
+}
+
+TEST_F(ResolverConstAssertTest, Global_LessThan_Pass) {
+    GlobalConstAssert(LessThan(2_i, 3_i));
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverConstAssertTest, Global_LessThan_Fail) {
+    GlobalConstAssert(Source{{12, 34}}, LessThan(4_i, 3_i));
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: const assertion failed");
+}
+
+TEST_F(ResolverConstAssertTest, Local_True_Pass) {
+    WrapInFunction(ConstAssert(true));
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverConstAssertTest, Local_False_Fail) {
+    WrapInFunction(ConstAssert(Source{{12, 34}}, false));
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: const assertion failed");
+}
+
+TEST_F(ResolverConstAssertTest, Local_Const_Pass) {
+    GlobalConst("C", ty.bool_(), Expr(true));
+    WrapInFunction(ConstAssert("C"));
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverConstAssertTest, Local_Const_Fail) {
+    GlobalConst("C", ty.bool_(), Expr(false));
+    WrapInFunction(ConstAssert(Source{{12, 34}}, "C"));
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: const assertion failed");
+}
+
+TEST_F(ResolverConstAssertTest, Local_NonConst) {
+    GlobalVar("V", ty.bool_(), Expr(true), type::AddressSpace::kPrivate);
+    WrapInFunction(ConstAssert(Expr(Source{{12, 34}}, "V")));
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              "12:34 error: const assertion requires a const-expression, but expression is a "
+              "runtime-expression");
+}
+
+TEST_F(ResolverConstAssertTest, Local_LessThan_Pass) {
+    WrapInFunction(ConstAssert(LessThan(2_i, 3_i)));
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverConstAssertTest, Local_LessThan_Fail) {
+    WrapInFunction(ConstAssert(Source{{12, 34}}, LessThan(4_i, 3_i)));
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: const assertion failed");
+}
+
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/resolver/dependency_graph.cc b/src/tint/resolver/dependency_graph.cc
index 4047ce5..5ece264 100644
--- a/src/tint/resolver/dependency_graph.cc
+++ b/src/tint/resolver/dependency_graph.cc
@@ -31,6 +31,7 @@
 #include "src/tint/ast/continue_statement.h"
 #include "src/tint/ast/depth_multisampled_texture.h"
 #include "src/tint/ast/depth_texture.h"
+#include "src/tint/ast/diagnostic_attribute.h"
 #include "src/tint/ast/discard_statement.h"
 #include "src/tint/ast/external_texture.h"
 #include "src/tint/ast/f16.h"
@@ -204,10 +205,13 @@
                     TraverseExpression(var->initializer);
                 }
             },
-            [&](const ast::Enable*) {
-                // Enable directives do not effect the dependency graph.
+            [&](const ast::DiagnosticControl*) {
+                // Diagnostic control directives do not affect the dependency graph.
             },
-            [&](const ast::StaticAssert* assertion) { TraverseExpression(assertion->condition); },
+            [&](const ast::Enable*) {
+                // Enable directives do not affect the dependency graph.
+            },
+            [&](const ast::ConstAssert* assertion) { TraverseExpression(assertion->condition); },
             [&](Default) { UnhandledNode(diagnostics_, global->node); });
     }
 
@@ -319,7 +323,7 @@
                 TraverseExpression(w->condition);
                 TraverseStatement(w->body);
             },
-            [&](const ast::StaticAssert* assertion) { TraverseExpression(assertion->condition); },
+            [&](const ast::ConstAssert* assertion) { TraverseExpression(assertion->condition); },
             [&](Default) {
                 if (TINT_UNLIKELY((!stmt->IsAnyOf<ast::BreakStatement, ast::ContinueStatement,
                                                   ast::DiscardStatement>()))) {
@@ -454,9 +458,9 @@
             return;
         }
 
-        if (attr->IsAnyOf<ast::BuiltinAttribute, ast::InternalAttribute, ast::InterpolateAttribute,
-                          ast::InvariantAttribute, ast::StageAttribute, ast::StrideAttribute,
-                          ast::StructMemberOffsetAttribute>()) {
+        if (attr->IsAnyOf<ast::BuiltinAttribute, ast::DiagnosticAttribute, ast::InternalAttribute,
+                          ast::InterpolateAttribute, ast::InvariantAttribute, ast::StageAttribute,
+                          ast::StrideAttribute, ast::StructMemberOffsetAttribute>()) {
             return;
         }
 
@@ -555,8 +559,9 @@
             [&](const ast::TypeDecl* td) { return td->name; },
             [&](const ast::Function* func) { return func->symbol; },
             [&](const ast::Variable* var) { return var->symbol; },
+            [&](const ast::DiagnosticControl*) { return Symbol(); },
             [&](const ast::Enable*) { return Symbol(); },
-            [&](const ast::StaticAssert*) { return Symbol(); },
+            [&](const ast::ConstAssert*) { return Symbol(); },
             [&](Default) {
                 UnhandledNode(diagnostics_, node);
                 return Symbol{};
@@ -575,12 +580,12 @@
     /// 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::Variable* v) { return v->Kind(); },          //
-            [&](const ast::StaticAssert*) { return "static_assert"; },  //
+            node,                                                     //
+            [&](const ast::Struct*) { return "struct"; },             //
+            [&](const ast::Alias*) { return "alias"; },               //
+            [&](const ast::Function*) { return "function"; },         //
+            [&](const ast::Variable* v) { return v->Kind(); },        //
+            [&](const ast::ConstAssert*) { return "const_assert"; },  //
             [&](Default) {
                 UnhandledNode(diagnostics_, node);
                 return "<error>";
@@ -663,16 +668,16 @@
             return;  // This code assumes there are no undeclared identifiers.
         }
 
-        // Make sure all 'enable' directives go before any other global declarations.
+        // Make sure all directives go before any other global declarations.
         for (auto* global : declaration_order_) {
-            if (auto* enable = global->node->As<ast::Enable>()) {
-                sorted_.Add(enable);
+            if (global->node->IsAnyOf<ast::DiagnosticControl, ast::Enable>()) {
+                sorted_.Add(global->node);
             }
         }
 
         for (auto* global : declaration_order_) {
-            if (global->node->Is<ast::Enable>()) {
-                // Skip 'enable' directives here, as they are already added.
+            if (global->node->IsAnyOf<ast::DiagnosticControl, ast::Enable>()) {
+                // Skip directives here, as they are already added.
                 continue;
             }
             utils::UniqueVector<const Global*, 8> stack;
diff --git a/src/tint/resolver/dependency_graph_test.cc b/src/tint/resolver/dependency_graph_test.cc
index 75faf34..f1f0a0c 100644
--- a/src/tint/resolver/dependency_graph_test.cc
+++ b/src/tint/resolver/dependency_graph_test.cc
@@ -1088,18 +1088,19 @@
                          testing::Combine(testing::ValuesIn(kFuncDeclKinds),
                                           testing::ValuesIn(kFuncUseKinds)));
 
-TEST_F(ResolverDependencyGraphOrderedGlobalsTest, EnableFirst) {
-    // Test that enable nodes always go before any other global declaration.
-    // Although all enable directives in a valid WGSL program must go before any other global
-    // declaration, a transform may produce such a AST tree that has some declarations before enable
-    // nodes. DependencyGraph should deal with these cases.
+TEST_F(ResolverDependencyGraphOrderedGlobalsTest, DirectiveFirst) {
+    // Test that directive nodes always go before any other global declaration.
+    // Although all directives in a valid WGSL program must go before any other global declaration,
+    // a transform may produce such a AST tree that has some declarations before directive nodes.
+    // DependencyGraph should deal with these cases.
     auto* var_1 = GlobalVar("SYMBOL1", ty.i32());
-    auto* enable_1 = Enable(ast::Extension::kF16);
+    auto* enable = Enable(ast::Extension::kF16);
     auto* var_2 = GlobalVar("SYMBOL2", ty.f32());
-    auto* enable_2 = Enable(ast::Extension::kF16);
+    auto* diagnostic = DiagnosticControl(ast::DiagnosticSeverity::kWarning, Expr("foo"));
+    AST().AddDiagnosticControl(diagnostic);
 
-    EXPECT_THAT(AST().GlobalDeclarations(), ElementsAre(var_1, enable_1, var_2, enable_2));
-    EXPECT_THAT(Build().ordered_globals, ElementsAre(enable_1, enable_2, var_1, var_2));
+    EXPECT_THAT(AST().GlobalDeclarations(), ElementsAre(var_1, enable, var_2, diagnostic));
+    EXPECT_THAT(Build().ordered_globals, ElementsAre(enable, diagnostic, var_1, var_2));
 }
 }  // namespace ordered_globals
 
diff --git a/src/tint/resolver/diagnostic_control_test.cc b/src/tint/resolver/diagnostic_control_test.cc
new file mode 100644
index 0000000..6c217e6
--- /dev/null
+++ b/src/tint/resolver/diagnostic_control_test.cc
@@ -0,0 +1,185 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/resolver/resolver.h"
+
+#include "src/tint/resolver/resolver_test_helper.h"
+
+using namespace tint::number_suffixes;  // NOLINT
+
+namespace tint::resolver {
+namespace {
+
+using ResolverDiagnosticControlTest = ResolverTest;
+
+TEST_F(ResolverDiagnosticControlTest, UnreachableCode_DefaultSeverity) {
+    auto stmts = utils::Vector{Return(), Return()};
+    Func("foo", {}, ty.void_(), stmts);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), R"(warning: code is unreachable)");
+}
+
+TEST_F(ResolverDiagnosticControlTest, UnreachableCode_ErrorViaDirective) {
+    DiagnosticDirective(ast::DiagnosticSeverity::kError, Expr("chromium_unreachable_code"));
+
+    auto stmts = utils::Vector{Return(), Return()};
+    Func("foo", {}, ty.void_(), stmts);
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), R"(error: code is unreachable)");
+}
+
+TEST_F(ResolverDiagnosticControlTest, UnreachableCode_WarningViaDirective) {
+    DiagnosticDirective(ast::DiagnosticSeverity::kWarning, Expr("chromium_unreachable_code"));
+
+    auto stmts = utils::Vector{Return(), Return()};
+    Func("foo", {}, ty.void_(), stmts);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), R"(warning: code is unreachable)");
+}
+
+TEST_F(ResolverDiagnosticControlTest, UnreachableCode_InfoViaDirective) {
+    DiagnosticDirective(ast::DiagnosticSeverity::kInfo, Expr("chromium_unreachable_code"));
+
+    auto stmts = utils::Vector{Return(), Return()};
+    Func("foo", {}, ty.void_(), stmts);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), R"(note: code is unreachable)");
+}
+
+TEST_F(ResolverDiagnosticControlTest, UnreachableCode_OffViaDirective) {
+    DiagnosticDirective(ast::DiagnosticSeverity::kOff, Expr("chromium_unreachable_code"));
+
+    auto stmts = utils::Vector{Return(), Return()};
+    Func("foo", {}, ty.void_(), stmts);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_TRUE(r()->error().empty());
+}
+
+TEST_F(ResolverDiagnosticControlTest, UnreachableCode_ErrorViaAttribute) {
+    auto* attr =
+        DiagnosticAttribute(ast::DiagnosticSeverity::kError, Expr("chromium_unreachable_code"));
+
+    auto stmts = utils::Vector{Return(), Return()};
+    Func("foo", {}, ty.void_(), stmts, utils::Vector{attr});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), R"(error: code is unreachable)");
+}
+
+TEST_F(ResolverDiagnosticControlTest, UnreachableCode_WarningViaAttribute) {
+    auto* attr =
+        DiagnosticAttribute(ast::DiagnosticSeverity::kWarning, Expr("chromium_unreachable_code"));
+
+    auto stmts = utils::Vector{Return(), Return()};
+    Func("foo", {}, ty.void_(), stmts, utils::Vector{attr});
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), R"(warning: code is unreachable)");
+}
+
+TEST_F(ResolverDiagnosticControlTest, UnreachableCode_InfoViaAttribute) {
+    auto* attr =
+        DiagnosticAttribute(ast::DiagnosticSeverity::kInfo, Expr("chromium_unreachable_code"));
+
+    auto stmts = utils::Vector{Return(), Return()};
+    Func("foo", {}, ty.void_(), stmts, utils::Vector{attr});
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), R"(note: code is unreachable)");
+}
+
+TEST_F(ResolverDiagnosticControlTest, UnreachableCode_OffViaAttribute) {
+    auto* attr =
+        DiagnosticAttribute(ast::DiagnosticSeverity::kOff, Expr("chromium_unreachable_code"));
+
+    auto stmts = utils::Vector{Return(), Return()};
+    Func("foo", {}, ty.void_(), stmts, utils::Vector{attr});
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_TRUE(r()->error().empty());
+}
+
+TEST_F(ResolverDiagnosticControlTest, UnreachableCode_ErrorViaDirective_OverriddenViaAttribute) {
+    // diagnostic(error, chromium_unreachable_code);
+    //
+    // @diagnostic(off, chromium_unreachable_code) fn foo() {
+    //   return;
+    //   return; // Should produce a warning
+    // }
+    DiagnosticDirective(ast::DiagnosticSeverity::kError, Expr("chromium_unreachable_code"));
+    auto* attr =
+        DiagnosticAttribute(ast::DiagnosticSeverity::kWarning, Expr("chromium_unreachable_code"));
+
+    auto stmts = utils::Vector{Return(), Return()};
+    Func("foo", {}, ty.void_(), stmts, utils::Vector{attr});
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), R"(warning: code is unreachable)");
+}
+
+TEST_F(ResolverDiagnosticControlTest, FunctionAttributeScope) {
+    // @diagnostic(off, chromium_unreachable_code) fn foo() {
+    //   return;
+    //   return; // Should not produce a diagnostic
+    // }
+    //
+    // fn zoo() {
+    //   return;
+    //   return; // Should produce a warning (default severity)
+    // }
+    //
+    // @diagnostic(info, chromium_unreachable_code) fn bar() {
+    //   return;
+    //   return; // Should produce an info
+    // }
+    {
+        auto* attr =
+            DiagnosticAttribute(ast::DiagnosticSeverity::kOff, Expr("chromium_unreachable_code"));
+        Func("foo", {}, ty.void_(),
+             utils::Vector{
+                 Return(),
+                 Return(Source{{12, 34}}),
+             },
+             utils::Vector{attr});
+    }
+    {
+        Func("bar", {}, ty.void_(),
+             utils::Vector{
+                 Return(),
+                 Return(Source{{45, 67}}),
+             });
+    }
+    {
+        auto* attr =
+            DiagnosticAttribute(ast::DiagnosticSeverity::kInfo, Expr("chromium_unreachable_code"));
+        Func("zoo", {}, ty.void_(),
+             utils::Vector{
+                 Return(),
+                 Return(Source{{89, 10}}),
+             },
+             utils::Vector{attr});
+    }
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), R"(45:67 warning: code is unreachable
+89:10 note: code is unreachable)");
+}
+
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 3f306b5..7db9a2f 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -136,9 +136,20 @@
         return false;
     }
 
-    // Create the semantic module
-    builder_->Sem().SetModule(builder_->create<sem::Module>(
-        std::move(dependencies_.ordered_globals), std::move(enabled_extensions_)));
+    // Create the semantic module.
+    auto* mod = builder_->create<sem::Module>(std::move(dependencies_.ordered_globals),
+                                              std::move(enabled_extensions_));
+    ApplyDiagnosticSeverities(mod);
+    builder_->Sem().SetModule(mod);
+
+    if (result) {
+        // Run the uniformity analysis, which requires a complete semantic module.
+        if (!enabled_extensions_.Contains(ast::Extension::kChromiumDisableUniformityAnalysis)) {
+            if (!AnalyzeUniformity(builder_, dependencies_)) {
+                return false;
+            }
+        }
+    }
 
     return result;
 }
@@ -151,11 +162,12 @@
         Mark(decl);
         if (!Switch<bool>(
                 decl,  //
+                [&](const ast::DiagnosticControl* dc) { return DiagnosticControl(dc); },
                 [&](const ast::Enable* e) { return Enable(e); },
                 [&](const ast::TypeDecl* td) { return TypeDecl(td); },
                 [&](const ast::Function* func) { return Function(func); },
                 [&](const ast::Variable* var) { return GlobalVariable(var); },
-                [&](const ast::StaticAssert* sa) { return StaticAssert(sa); },
+                [&](const ast::ConstAssert* ca) { return ConstAssert(ca); },
                 [&](Default) {
                     TINT_UNREACHABLE(Resolver, diagnostics_)
                         << "unhandled global declaration: " << decl->TypeInfo().name;
@@ -179,14 +191,6 @@
         return false;
     }
 
-    if (!enabled_extensions_.Contains(ast::Extension::kChromiumDisableUniformityAnalysis)) {
-        if (!AnalyzeUniformity(builder_, dependencies_)) {
-            if (kUniformityFailuresAsError) {
-                return false;
-            }
-        }
-    }
-
     bool result = true;
     for (auto* node : builder_->ASTNodes().Objects()) {
         if (TINT_UNLIKELY(!marked_[node->node_id.value])) {
@@ -936,8 +940,8 @@
     return sem;
 }
 
-sem::Statement* Resolver::StaticAssert(const ast::StaticAssert* assertion) {
-    ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "static assertion"};
+sem::Statement* Resolver::ConstAssert(const ast::ConstAssert* assertion) {
+    ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "const assertion"};
     TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
     auto* expr = Expression(assertion->condition);
     if (!expr) {
@@ -946,12 +950,12 @@
     auto* cond = expr->ConstantValue();
     if (auto* ty = cond->Type(); !ty->Is<type::Bool>()) {
         AddError(
-            "static assertion condition must be a bool, got '" + builder_->FriendlyName(ty) + "'",
+            "const assertion condition must be a bool, got '" + builder_->FriendlyName(ty) + "'",
             assertion->condition->source);
         return nullptr;
     }
     if (!cond->ValueAs<bool>()) {
-        AddError("static assertion failed", assertion->source);
+        AddError("const assertion failed", assertion->source);
         return nullptr;
     }
     auto* sem =
@@ -965,6 +969,21 @@
     utils::Hashmap<Symbol, Source, 8> parameter_names;
     utils::Vector<sem::Parameter*, 8> parameters;
 
+    validator_.DiagnosticFilters().Push();
+    TINT_DEFER(validator_.DiagnosticFilters().Pop());
+    for (auto* attr : decl->attributes) {
+        Mark(attr);
+        if (auto* dc = attr->As<ast::DiagnosticAttribute>()) {
+            Mark(dc->control);
+            if (!DiagnosticControl(dc->control)) {
+                return nullptr;
+            }
+        }
+    }
+    if (!validator_.NoDuplicateAttributes(decl->attributes)) {
+        return nullptr;
+    }
+
     // Resolve all the parameters
     for (auto* param : decl->params) {
         Mark(param);
@@ -1031,9 +1050,6 @@
             return_location = value.Get();
         }
     }
-    if (!validator_.NoDuplicateAttributes(decl->attributes)) {
-        return nullptr;
-    }
 
     if (auto* str = return_type->As<sem::Struct>()) {
         if (!ApplyAddressSpaceUsageToType(type::AddressSpace::kNone, str, decl->source)) {
@@ -1060,6 +1076,7 @@
 
     auto* func =
         builder_->create<sem::Function>(decl, return_type, return_location, std::move(parameters));
+    ApplyDiagnosticSeverities(func);
     builder_->Sem().Add(decl, func);
 
     TINT_SCOPED_ASSIGNMENT(current_function_, func);
@@ -1095,10 +1112,6 @@
         }
     }
 
-    for (auto* attr : decl->attributes) {
-        Mark(attr);
-    }
-
     if (!validator_.NoDuplicateAttributes(decl->return_type_attributes)) {
         return nullptr;
     }
@@ -1258,7 +1271,7 @@
         [&](const ast::IncrementDecrementStatement* i) { return IncrementDecrementStatement(i); },
         [&](const ast::ReturnStatement* r) { return ReturnStatement(r); },
         [&](const ast::VariableDeclStatement* v) { return VariableDeclStatement(v); },
-        [&](const ast::StaticAssert* sa) { return StaticAssert(sa); },
+        [&](const ast::ConstAssert* sa) { return ConstAssert(sa); },
 
         // Error cases
         [&](const ast::CaseStatement*) {
@@ -3050,6 +3063,16 @@
     return sem;
 }
 
+bool Resolver::DiagnosticControl(const ast::DiagnosticControl* control) {
+    Mark(control->rule_name);
+    auto rule_name = builder_->Symbols().NameFor(control->rule_name->symbol);
+    auto rule = ast::ParseDiagnosticRule(rule_name);
+    if (rule != ast::DiagnosticRule::kUndefined) {
+        validator_.DiagnosticFilters().Set(rule, control->severity);
+    }
+    return true;
+}
+
 bool Resolver::Enable(const ast::Enable* enable) {
     enabled_extensions_.Add(enable->extension);
     return true;
@@ -3859,6 +3882,13 @@
     return false;
 }
 
+template <typename NODE>
+void Resolver::ApplyDiagnosticSeverities(NODE* node) {
+    for (auto itr : validator_.DiagnosticFilters().Top()) {
+        node->SetDiagnosticSeverity(itr.key, itr.value);
+    }
+}
+
 void Resolver::AddError(const std::string& msg, const Source& source) const {
     diagnostics_.add_error(diag::System::Resolver, msg, source);
 }
diff --git a/src/tint/resolver/resolver.h b/src/tint/resolver/resolver.h
index 38d08c0..519cea8 100644
--- a/src/tint/resolver/resolver.h
+++ b/src/tint/resolver/resolver.h
@@ -30,7 +30,6 @@
 #include "src/tint/resolver/intrinsic_table.h"
 #include "src/tint/resolver/sem_helper.h"
 #include "src/tint/resolver/validator.h"
-#include "src/tint/scope_stack.h"
 #include "src/tint/sem/binding_point.h"
 #include "src/tint/sem/block_statement.h"
 #include "src/tint/sem/function.h"
@@ -230,6 +229,7 @@
     sem::CaseStatement* CaseStatement(const ast::CaseStatement*, const type::Type*);
     sem::Statement* CompoundAssignmentStatement(const ast::CompoundAssignmentStatement*);
     sem::Statement* ContinueStatement(const ast::ContinueStatement*);
+    sem::Statement* ConstAssert(const ast::ConstAssert*);
     sem::Statement* DiscardStatement(const ast::DiscardStatement*);
     sem::ForLoopStatement* ForLoopStatement(const ast::ForLoopStatement*);
     sem::WhileStatement* WhileStatement(const ast::WhileStatement*);
@@ -240,7 +240,6 @@
     sem::LoopStatement* LoopStatement(const ast::LoopStatement*);
     sem::Statement* ReturnStatement(const ast::ReturnStatement*);
     sem::Statement* Statement(const ast::Statement*);
-    sem::Statement* StaticAssert(const ast::StaticAssert*);
     sem::SwitchStatement* SwitchStatement(const ast::SwitchStatement* s);
     sem::Statement* VariableDeclStatement(const ast::VariableDeclStatement*);
     bool Statements(utils::VectorRef<const ast::Statement*>);
@@ -262,6 +261,10 @@
     /// @param ty the ast::Type
     type::Type* Type(const ast::Type* ty);
 
+    /// @param control the diagnostic control
+    /// @returns true on success, false on failure
+    bool DiagnosticControl(const ast::DiagnosticControl* control);
+
     /// @param enable the enable declaration
     /// @returns the resolved extension
     bool Enable(const ast::Enable* enable);
@@ -411,6 +414,11 @@
     /// @returns true on success, false on error
     bool Mark(const ast::Node* node);
 
+    /// Applies the diagnostic severities from the current scope to a semantic node.
+    /// @param node the semantic node to apply the diagnostic severities to
+    template <typename NODE>
+    void ApplyDiagnosticSeverities(NODE* node);
+
     /// Adds the given error message to the diagnostics
     void AddError(const std::string& msg, const Source& source) const;
 
diff --git a/src/tint/resolver/static_assert_test.cc b/src/tint/resolver/static_assert_test.cc
deleted file mode 100644
index 3915dcd..0000000
--- a/src/tint/resolver/static_assert_test.cc
+++ /dev/null
@@ -1,107 +0,0 @@
-// Copyright 2022 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "src/tint/resolver/resolver.h"
-
-#include "gmock/gmock.h"
-#include "src/tint/resolver/resolver_test_helper.h"
-
-using namespace tint::number_suffixes;  // NOLINT
-
-namespace tint::resolver {
-namespace {
-
-using ResolverStaticAssertTest = ResolverTest;
-
-TEST_F(ResolverStaticAssertTest, Global_True_Pass) {
-    GlobalStaticAssert(true);
-    ASSERT_TRUE(r()->Resolve()) << r()->error();
-}
-
-TEST_F(ResolverStaticAssertTest, Global_False_Fail) {
-    GlobalStaticAssert(Source{{12, 34}}, false);
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: static assertion failed");
-}
-
-TEST_F(ResolverStaticAssertTest, Global_Const_Pass) {
-    GlobalConst("C", ty.bool_(), Expr(true));
-    GlobalStaticAssert("C");
-    ASSERT_TRUE(r()->Resolve()) << r()->error();
-}
-
-TEST_F(ResolverStaticAssertTest, Global_Const_Fail) {
-    GlobalConst("C", ty.bool_(), Expr(false));
-    GlobalStaticAssert(Source{{12, 34}}, "C");
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: static assertion failed");
-}
-
-TEST_F(ResolverStaticAssertTest, Global_LessThan_Pass) {
-    GlobalStaticAssert(LessThan(2_i, 3_i));
-    ASSERT_TRUE(r()->Resolve()) << r()->error();
-}
-
-TEST_F(ResolverStaticAssertTest, Global_LessThan_Fail) {
-    GlobalStaticAssert(Source{{12, 34}}, LessThan(4_i, 3_i));
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: static assertion failed");
-}
-
-TEST_F(ResolverStaticAssertTest, Local_True_Pass) {
-    WrapInFunction(StaticAssert(true));
-    ASSERT_TRUE(r()->Resolve()) << r()->error();
-}
-
-TEST_F(ResolverStaticAssertTest, Local_False_Fail) {
-    WrapInFunction(StaticAssert(Source{{12, 34}}, false));
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: static assertion failed");
-}
-
-TEST_F(ResolverStaticAssertTest, Local_Const_Pass) {
-    GlobalConst("C", ty.bool_(), Expr(true));
-    WrapInFunction(StaticAssert("C"));
-    ASSERT_TRUE(r()->Resolve()) << r()->error();
-}
-
-TEST_F(ResolverStaticAssertTest, Local_Const_Fail) {
-    GlobalConst("C", ty.bool_(), Expr(false));
-    WrapInFunction(StaticAssert(Source{{12, 34}}, "C"));
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: static assertion failed");
-}
-
-TEST_F(ResolverStaticAssertTest, Local_NonConst) {
-    GlobalVar("V", ty.bool_(), Expr(true), type::AddressSpace::kPrivate);
-    WrapInFunction(StaticAssert(Expr(Source{{12, 34}}, "V")));
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: static assertion requires a const-expression, but expression is a "
-              "runtime-expression");
-}
-
-TEST_F(ResolverStaticAssertTest, Local_LessThan_Pass) {
-    WrapInFunction(StaticAssert(LessThan(2_i, 3_i)));
-    ASSERT_TRUE(r()->Resolve()) << r()->error();
-}
-
-TEST_F(ResolverStaticAssertTest, Local_LessThan_Fail) {
-    WrapInFunction(StaticAssert(Source{{12, 34}}, LessThan(4_i, 3_i)));
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: static assertion failed");
-}
-
-}  // namespace
-}  // namespace tint::resolver
diff --git a/src/tint/resolver/uniformity.cc b/src/tint/resolver/uniformity.cc
index 7b297e1..ec10d92 100644
--- a/src/tint/resolver/uniformity.cc
+++ b/src/tint/resolver/uniformity.cc
@@ -66,9 +66,12 @@
 }
 
 /// CallSiteTag describes the uniformity requirements on the call sites of a function.
-enum CallSiteTag {
-    CallSiteRequiredToBeUniform,
-    CallSiteNoRestriction,
+struct CallSiteTag {
+    enum {
+        CallSiteRequiredToBeUniform,
+        CallSiteNoRestriction,
+    } tag;
+    ast::DiagnosticSeverity severity = ast::DiagnosticSeverity::kUndefined;
 };
 
 /// FunctionTag describes a functions effects on uniformity.
@@ -78,10 +81,13 @@
 };
 
 /// ParameterTag describes the uniformity requirements of values passed to a function parameter.
-enum ParameterTag {
-    ParameterValueRequiredToBeUniform,
-    ParameterContentsRequiredToBeUniform,
-    ParameterNoRestriction,
+struct ParameterTag {
+    enum {
+        ParameterValueRequiredToBeUniform,
+        ParameterContentsRequiredToBeUniform,
+        ParameterNoRestriction,
+    } tag;
+    ast::DiagnosticSeverity severity = ast::DiagnosticSeverity::kUndefined;
 };
 
 /// Node represents a node in the graph of control flow and value nodes within the analysis of a
@@ -138,9 +144,9 @@
     /// The semantic node in corresponds to this parameter.
     const sem::Parameter* sem;
     /// The parameter's direct uniformity requirements.
-    ParameterTag tag_direct = ParameterNoRestriction;
+    ParameterTag tag_direct = {ParameterTag::ParameterNoRestriction};
     /// The parameter's uniformity requirements that affect the function return value.
-    ParameterTag tag_retval = ParameterNoRestriction;
+    ParameterTag tag_retval = {ParameterTag::ParameterNoRestriction};
     /// Will be `true` if this function may cause the contents of this pointer parameter to become
     /// non-uniform.
     bool pointer_may_become_non_uniform = false;
@@ -166,11 +172,13 @@
     /// @param builder the program builder
     FunctionInfo(const ast::Function* func, const ProgramBuilder* builder) {
         name = builder->Symbols().NameFor(func->symbol);
-        callsite_tag = CallSiteNoRestriction;
+        callsite_tag = {CallSiteTag::CallSiteNoRestriction};
         function_tag = NoRestriction;
 
         // Create special nodes.
-        required_to_be_uniform = CreateNode({"RequiredToBeUniform"});
+        required_to_be_uniform_error = CreateNode({"RequiredToBeUniform_Error"});
+        required_to_be_uniform_warning = CreateNode({"RequiredToBeUniform_Warning"});
+        required_to_be_uniform_info = CreateNode({"RequiredToBeUniform_Info"});
         may_be_non_uniform = CreateNode({"MayBeNonUniform"});
         cf_start = CreateNode({"CF_start"});
         if (func->return_type) {
@@ -214,14 +222,16 @@
     /// The control flow graph.
     utils::BlockAllocator<Node> nodes;
 
-    /// Special `RequiredToBeUniform` node.
-    Node* required_to_be_uniform;
+    /// Special `RequiredToBeUniform` nodes.
+    Node* required_to_be_uniform_error = nullptr;
+    Node* required_to_be_uniform_warning = nullptr;
+    Node* required_to_be_uniform_info = nullptr;
     /// Special `MayBeNonUniform` node.
-    Node* may_be_non_uniform;
+    Node* may_be_non_uniform = nullptr;
     /// Special `CF_start` node.
-    Node* cf_start;
+    Node* cf_start = nullptr;
     /// Special `Value_return` node.
-    Node* value_return;
+    Node* value_return = nullptr;
 
     /// Map from variables to their value nodes in the graph, scoped with respect to control flow.
     ScopeStack<const sem::Variable*, Node*> variables;
@@ -246,6 +256,21 @@
         utils::Hashmap<const sem::Variable*, Node*, 4> var_exit_nodes;
     };
 
+    /// @returns the RequiredToBeUniform node that corresponds to `severity`
+    Node* RequiredToBeUniform(ast::DiagnosticSeverity severity) {
+        switch (severity) {
+            case ast::DiagnosticSeverity::kError:
+                return required_to_be_uniform_error;
+            case ast::DiagnosticSeverity::kWarning:
+                return required_to_be_uniform_warning;
+            case ast::DiagnosticSeverity::kInfo:
+                return required_to_be_uniform_info;
+            default:
+                TINT_ASSERT(Resolver, false && "unhandled severity");
+                return nullptr;
+        }
+    }
+
     /// @returns a LoopSwitchInfo for the given statement, allocating the LoopSwitchInfo if this is
     /// the first call with the given statement.
     LoopSwitchInfo& LoopSwitchInfoFor(const sem::Statement* stmt) {
@@ -401,32 +426,49 @@
                 // For pointers, we distinguish between requiring uniformity of the contents versus
                 // the pointer itself.
                 if (reachable.Contains(param_info.ptr_input_contents)) {
-                    return ParameterContentsRequiredToBeUniform;
+                    return ParameterTag::ParameterContentsRequiredToBeUniform;
                 } else if (reachable.Contains(param_info.value)) {
-                    return ParameterValueRequiredToBeUniform;
+                    return ParameterTag::ParameterValueRequiredToBeUniform;
                 }
             } else if (reachable.Contains(current_function_->variables.Get(param))) {
                 // For non-pointers, the requirement is always on the value.
-                return ParameterValueRequiredToBeUniform;
+                return ParameterTag::ParameterValueRequiredToBeUniform;
             }
-            return ParameterNoRestriction;
+            return ParameterTag::ParameterNoRestriction;
         };
 
         // Look at which nodes are reachable from "RequiredToBeUniform".
         {
             utils::UniqueVector<Node*, 4> reachable;
-            Traverse(current_function_->required_to_be_uniform, &reachable);
-            if (reachable.Contains(current_function_->may_be_non_uniform)) {
-                MakeError(*current_function_, current_function_->may_be_non_uniform);
-                return false;
-            }
-            if (reachable.Contains(current_function_->cf_start)) {
-                current_function_->callsite_tag = CallSiteRequiredToBeUniform;
-            }
+            auto traverse = [&](ast::DiagnosticSeverity severity) {
+                Traverse(current_function_->RequiredToBeUniform(severity), &reachable);
+                if (reachable.Contains(current_function_->may_be_non_uniform)) {
+                    MakeError(*current_function_, current_function_->may_be_non_uniform, severity);
+                    return false;
+                }
+                if (reachable.Contains(current_function_->cf_start)) {
+                    if (current_function_->callsite_tag.tag == CallSiteTag::CallSiteNoRestriction) {
+                        current_function_->callsite_tag = {CallSiteTag::CallSiteRequiredToBeUniform,
+                                                           severity};
+                    }
+                }
 
-            // Set the tags to capture the direct uniformity requirements of each parameter.
-            for (size_t i = 0; i < func->params.Length(); i++) {
-                current_function_->parameters[i].tag_direct = get_param_tag(reachable, i);
+                // Set the tags to capture the direct uniformity requirements of each parameter.
+                for (size_t i = 0; i < func->params.Length(); i++) {
+                    if (current_function_->parameters[i].tag_direct.tag ==
+                        ParameterTag::ParameterNoRestriction) {
+                        current_function_->parameters[i].tag_direct = {get_param_tag(reachable, i),
+                                                                       severity};
+                    }
+                }
+                return true;
+            };
+            if (!traverse(ast::DiagnosticSeverity::kError)) {
+                return false;
+            } else {
+                if (traverse(ast::DiagnosticSeverity::kWarning)) {
+                    traverse(ast::DiagnosticSeverity::kInfo);
+                }
             }
         }
 
@@ -441,7 +483,7 @@
             // Set the tags to capture the uniformity requirements of each parameter with respect to
             // the function return value.
             for (size_t i = 0; i < func->params.Length(); i++) {
-                current_function_->parameters[i].tag_retval = get_param_tag(reachable, i);
+                current_function_->parameters[i].tag_retval = {get_param_tag(reachable, i)};
             }
         }
 
@@ -467,9 +509,9 @@
             for (size_t j = 0; j < func->params.Length(); j++) {
                 auto tag = get_param_tag(reachable, j);
                 auto* source_param = sem_.Get<sem::Parameter>(func->params[j]);
-                if (tag == ParameterContentsRequiredToBeUniform) {
+                if (tag == ParameterTag::ParameterContentsRequiredToBeUniform) {
                     param_info.ptr_output_source_param_contents.Push(source_param);
-                } else if (tag == ParameterValueRequiredToBeUniform) {
+                } else if (tag == ParameterTag::ParameterValueRequiredToBeUniform) {
                     param_info.ptr_output_source_param_values.Push(source_param);
                 }
             }
@@ -1067,7 +1109,7 @@
                 return cf;
             },
 
-            [&](const ast::StaticAssert*) {
+            [&](const ast::ConstAssert*) {
                 return cf;  // No impact on uniformity
             },
 
@@ -1444,8 +1486,11 @@
         result->type = Node::kFunctionCallReturnValue;
         Node* cf_after = CreateNode({"CF_after_", name}, call);
 
+        auto default_severity = kUniformityFailuresAsError ? ast::DiagnosticSeverity::kError
+                                                           : ast::DiagnosticSeverity::kWarning;
+
         // Get tags for the callee.
-        CallSiteTag callsite_tag = CallSiteNoRestriction;
+        CallSiteTag callsite_tag = {CallSiteTag::CallSiteNoRestriction};
         FunctionTag function_tag = NoRestriction;
         auto* sem = SemCall(call);
         const FunctionInfo* func_info = nullptr;
@@ -1455,21 +1500,23 @@
                 // Most builtins have no restrictions. The exceptions are barriers, derivatives,
                 // some texture sampling builtins, and atomics.
                 if (builtin->IsBarrier()) {
-                    callsite_tag = CallSiteRequiredToBeUniform;
+                    callsite_tag = {CallSiteTag::CallSiteRequiredToBeUniform, default_severity};
                 } else if (builtin->Type() == sem::BuiltinType::kWorkgroupUniformLoad) {
-                    callsite_tag = CallSiteRequiredToBeUniform;
+                    callsite_tag = {CallSiteTag::CallSiteRequiredToBeUniform, default_severity};
                 } else if (builtin->IsDerivative() ||
                            builtin->Type() == sem::BuiltinType::kTextureSample ||
                            builtin->Type() == sem::BuiltinType::kTextureSampleBias ||
                            builtin->Type() == sem::BuiltinType::kTextureSampleCompare) {
-                    callsite_tag = CallSiteRequiredToBeUniform;
-                    function_tag = ReturnValueMayBeNonUniform;
+                    // Get the severity of derivative uniformity violations in this context.
+                    auto severity =
+                        sem_.DiagnosticSeverity(call, ast::DiagnosticRule::kDerivativeUniformity);
+                    if (severity != ast::DiagnosticSeverity::kOff) {
+                        callsite_tag = {CallSiteTag::CallSiteRequiredToBeUniform, severity};
+                        function_tag = ReturnValueMayBeNonUniform;
+                    }
                 } else if (builtin->IsAtomic()) {
-                    callsite_tag = CallSiteNoRestriction;
+                    callsite_tag = {CallSiteTag::CallSiteNoRestriction};
                     function_tag = ReturnValueMayBeNonUniform;
-                } else {
-                    callsite_tag = CallSiteNoRestriction;
-                    function_tag = NoRestriction;
                 }
             },
             [&](const sem::Function* func) {
@@ -1482,11 +1529,11 @@
                 func_info = info;
             },
             [&](const sem::TypeInitializer*) {
-                callsite_tag = CallSiteNoRestriction;
+                callsite_tag = {CallSiteTag::CallSiteNoRestriction};
                 function_tag = NoRestriction;
             },
             [&](const sem::TypeConversion*) {
-                callsite_tag = CallSiteNoRestriction;
+                callsite_tag = {CallSiteTag::CallSiteNoRestriction};
                 function_tag = NoRestriction;
             },
             [&](Default) {
@@ -1507,27 +1554,29 @@
                 auto& param_info = func_info->parameters[i];
 
                 // Capture the direct uniformity requirements.
-                switch (param_info.tag_direct) {
-                    case ParameterValueRequiredToBeUniform:
-                        current_function_->required_to_be_uniform->AddEdge(args[i]);
+                switch (param_info.tag_direct.tag) {
+                    case ParameterTag::ParameterValueRequiredToBeUniform:
+                        current_function_->RequiredToBeUniform(param_info.tag_direct.severity)
+                            ->AddEdge(args[i]);
                         break;
-                    case ParameterContentsRequiredToBeUniform: {
-                        current_function_->required_to_be_uniform->AddEdge(ptrarg_contents[i]);
+                    case ParameterTag::ParameterContentsRequiredToBeUniform: {
+                        current_function_->RequiredToBeUniform(param_info.tag_direct.severity)
+                            ->AddEdge(ptrarg_contents[i]);
                         break;
                     }
-                    case ParameterNoRestriction:
+                    case ParameterTag::ParameterNoRestriction:
                         break;
                 }
                 // Capture the effects of this parameter on the return value.
-                switch (param_info.tag_retval) {
-                    case ParameterValueRequiredToBeUniform:
+                switch (param_info.tag_retval.tag) {
+                    case ParameterTag::ParameterValueRequiredToBeUniform:
                         result->AddEdge(args[i]);
                         break;
-                    case ParameterContentsRequiredToBeUniform: {
+                    case ParameterTag::ParameterContentsRequiredToBeUniform: {
                         result->AddEdge(ptrarg_contents[i]);
                         break;
                     }
-                    case ParameterNoRestriction:
+                    case ParameterTag::ParameterNoRestriction:
                         break;
                 }
 
@@ -1566,7 +1615,7 @@
                 auto* builtin = sem->Target()->As<sem::Builtin>();
                 if (builtin && builtin->Type() == sem::BuiltinType::kWorkgroupUniformLoad) {
                     // The workgroupUniformLoad builtin requires its parameter to be uniform.
-                    current_function_->required_to_be_uniform->AddEdge(args[i]);
+                    current_function_->RequiredToBeUniform(default_severity)->AddEdge(args[i]);
                 } else {
                     // All other builtin function parameters are RequiredToBeUniformForReturnValue,
                     // as are parameters for type initializers and type conversions.
@@ -1578,8 +1627,8 @@
         // Add the callsite requirement last.
         // We traverse edges in reverse order, so this makes the callsite requirement take highest
         // priority when reporting violations.
-        if (callsite_tag == CallSiteRequiredToBeUniform) {
-            current_function_->required_to_be_uniform->AddEdge(call_node);
+        if (callsite_tag.tag == CallSiteTag::CallSiteRequiredToBeUniform) {
+            current_function_->RequiredToBeUniform(callsite_tag.severity)->AddEdge(call_node);
         }
 
         return {cf_after, result};
@@ -1625,8 +1674,9 @@
     }
 
     /// Recursively descend through the function called by `call` and the functions that it calls in
-    /// order to find a call to a builtin function that requires uniformity.
-    const ast::CallExpression* FindBuiltinThatRequiresUniformity(const ast::CallExpression* call) {
+    /// order to find a call to a builtin function that requires uniformity with the given severity.
+    const ast::CallExpression* FindBuiltinThatRequiresUniformity(const ast::CallExpression* call,
+                                                                 ast::DiagnosticSeverity severity) {
         auto* target = SemCall(call)->Target();
         if (target->Is<sem::Builtin>()) {
             // This is a call to a builtin, so we must be done.
@@ -1635,10 +1685,10 @@
             // This is a call to a user-defined function, so inspect the functions called by that
             // function and look for one whose node has an edge from the RequiredToBeUniform node.
             auto target_info = functions_.Find(user->Declaration());
-            for (auto* call_node : target_info->required_to_be_uniform->edges) {
+            for (auto* call_node : target_info->RequiredToBeUniform(severity)->edges) {
                 if (call_node->type == Node::kRegular) {
                     auto* child_call = call_node->ast->As<ast::CallExpression>();
-                    return FindBuiltinThatRequiresUniformity(child_call);
+                    return FindBuiltinThatRequiresUniformity(child_call, severity);
                 }
             }
             TINT_ASSERT(Resolver, false && "unable to find child call with uniformity requirement");
@@ -1783,16 +1833,15 @@
             });
     }
 
-    /// Generate an error message for a uniformity issue.
+    /// Generate a diagnostic message for a uniformity issue.
     /// @param function the function that the diagnostic is being produced for
     /// @param source_node the node that has caused a uniformity issue in `function`
-    void MakeError(FunctionInfo& function, Node* source_node) {
+    /// @param severity the severity of the diagnostic
+    void MakeError(FunctionInfo& function, Node* source_node, ast::DiagnosticSeverity severity) {
         // Helper to produce a diagnostic message, as a note or with the global failure severity.
         auto report = [&](Source source, std::string msg, bool note) {
             diag::Diagnostic error{};
-            auto failureSeverity =
-                kUniformityFailuresAsError ? diag::Severity::Error : diag::Severity::Warning;
-            error.severity = note ? diag::Severity::Note : failureSeverity;
+            error.severity = note ? diag::Severity::Note : ast::ToSeverity(severity);
             error.system = diag::System::Resolver;
             error.source = source;
             error.message = msg;
@@ -1801,12 +1850,12 @@
 
         // Traverse the graph to generate a path from RequiredToBeUniform to the source node.
         function.ResetVisited();
-        Traverse(function.required_to_be_uniform);
+        Traverse(function.RequiredToBeUniform(severity));
         TINT_ASSERT(Resolver, source_node->visited_from);
 
         // Find a node that is required to be uniform that has a path to the source node.
         auto* cause = TraceBackAlongPathUntil(source_node, [&](Node* node) {
-            return node->visited_from == function.required_to_be_uniform;
+            return node->visited_from == function.RequiredToBeUniform(severity);
         });
 
         // The node will always have a corresponding call expression.
@@ -1825,7 +1874,7 @@
                 auto next_function = functions_.Find(user_func->Declaration());
                 auto& param_info = next_function->parameters[cause->arg_index];
                 MakeError(*next_function,
-                          is_value ? param_info.value : param_info.ptr_input_contents);
+                          is_value ? param_info.value : param_info.ptr_input_contents, severity);
             }
 
             // Show the place where the non-uniform argument was passed.
@@ -1838,7 +1887,7 @@
             // Show the origin of non-uniformity for the value or data that is being passed.
             ShowSourceOfNonUniformity(source_node->visited_from);
         } else {
-            auto* builtin_call = FindBuiltinThatRequiresUniformity(call);
+            auto* builtin_call = FindBuiltinThatRequiresUniformity(call, severity);
             {
                 // Show a builtin was reachable from this call (which may be the call itself).
                 // This will be the trigger location for the failure.
diff --git a/src/tint/resolver/uniformity.h b/src/tint/resolver/uniformity.h
index 1139980..fd17449 100644
--- a/src/tint/resolver/uniformity.h
+++ b/src/tint/resolver/uniformity.h
@@ -26,7 +26,7 @@
 namespace tint::resolver {
 
 /// If true, uniformity analysis failures will be treated as an error, else as a warning.
-constexpr bool kUniformityFailuresAsError = false;
+constexpr bool kUniformityFailuresAsError = true;
 
 /// Analyze the uniformity of a program.
 /// @param builder the program to analyze
diff --git a/src/tint/resolver/uniformity_test.cc b/src/tint/resolver/uniformity_test.cc
index 2631976..274287a 100644
--- a/src/tint/resolver/uniformity_test.cc
+++ b/src/tint/resolver/uniformity_test.cc
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #include <memory>
+#include <sstream>
 #include <string>
 #include <tuple>
 #include <utility>
@@ -42,11 +43,7 @@
         bool valid = program.IsValid();
         if (should_pass) {
             EXPECT_TRUE(valid) << error_;
-            if (program.Diagnostics().count() == 1u) {
-                EXPECT_THAT(program.Diagnostics().str(), ::testing::HasSubstr("unreachable"));
-            } else {
-                EXPECT_EQ(program.Diagnostics().count(), 0u) << error_;
-            }
+            EXPECT_FALSE(program.Diagnostics().contains_errors());
         } else {
             if (kUniformityFailuresAsError) {
                 EXPECT_FALSE(valid);
@@ -407,7 +404,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:6:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:6:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -468,7 +465,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:10:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -511,7 +508,7 @@
     if (!should_pass) {
         EXPECT_EQ(
             error_,
-            R"(test:5:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+            R"(test:5:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -546,7 +543,7 @@
     if (!should_pass) {
         EXPECT_EQ(
             error_,
-            R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+            R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -591,7 +588,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:10:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -622,7 +619,7 @@
     RunTest(src, should_pass);
     if (!should_pass) {
         EXPECT_EQ(error_,
-                  R"(test:5:5 warning: 'dpdx' must only be called from uniform control flow
+                  R"(test:5:5 error: 'dpdx' must only be called from uniform control flow
     dpdx(0.5);
     ^^^^
 
@@ -656,7 +653,7 @@
     RunTest(src, should_pass);
     if (!should_pass) {
         EXPECT_EQ(error_,
-                  R"(test:9:5 warning: 'dpdx' must only be called from uniform control flow
+                  R"(test:9:5 error: 'dpdx' must only be called from uniform control flow
     dpdx(0.5);
     ^^^^
 
@@ -693,7 +690,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:5:5 warning: 'dpdx' must only be called from uniform control flow
+              R"(test:5:5 error: 'dpdx' must only be called from uniform control flow
     dpdx(0.5);
     ^^^^
 
@@ -723,7 +720,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'dpdx' must only be called from uniform control flow
+              R"(test:9:5 error: 'dpdx' must only be called from uniform control flow
     dpdx(0.5);
     ^^^^
 
@@ -836,7 +833,7 @@
         EXPECT_THAT(
             error_,
             ::testing::StartsWith(
-                R"(test:13:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+                R"(test:13:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();)"));
         EXPECT_THAT(error_,
                     ::testing::HasSubstr("test:14:9 note: reading from read_write storage buffer "
@@ -876,7 +873,7 @@
         EXPECT_THAT(
             error_,
             ::testing::StartsWith(
-                R"(test:14:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+                R"(test:14:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();)"));
         EXPECT_THAT(error_,
                     ::testing::HasSubstr("test:13:9 note: reading from read_write storage buffer "
@@ -917,7 +914,7 @@
         EXPECT_THAT(
             error_,
             ::testing::StartsWith(
-                R"(test:15:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+                R"(test:15:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();)"));
         EXPECT_THAT(error_,
                     ::testing::HasSubstr("test:13:9 note: reading from read_write storage buffer "
@@ -964,7 +961,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:7:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:7:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -1015,7 +1012,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -1083,7 +1080,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -1167,7 +1164,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:14:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:14:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -1205,7 +1202,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:15:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:15:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -1248,7 +1245,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:20:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:20:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -1322,7 +1319,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:20:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:20:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -1363,7 +1360,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -1407,7 +1404,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:16:9 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:16:9 error: 'workgroupBarrier' must only be called from uniform control flow
         workgroupBarrier();
         ^^^^^^^^^^^^^^^^
 
@@ -1448,7 +1445,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -1489,7 +1486,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -1607,7 +1604,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:16:9 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:16:9 error: 'workgroupBarrier' must only be called from uniform control flow
         workgroupBarrier();
         ^^^^^^^^^^^^^^^^
 
@@ -1669,7 +1666,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:6:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:6:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -1702,7 +1699,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -1757,7 +1754,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:10:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -1812,7 +1809,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -1873,7 +1870,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:15:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:15:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -1918,7 +1915,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:21:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:21:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -1960,7 +1957,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -2001,7 +1998,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -2038,7 +2035,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:7:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:7:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -2134,7 +2131,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:7:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:7:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -2171,7 +2168,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -2236,7 +2233,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:17:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:17:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -2283,7 +2280,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:23:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:23:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -2326,7 +2323,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -2369,7 +2366,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -2464,7 +2461,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:6:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:6:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -2492,7 +2489,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:7:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:7:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -2521,7 +2518,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -2550,7 +2547,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -2579,7 +2576,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -2608,7 +2605,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -2636,7 +2633,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:7:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:7:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -2667,7 +2664,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -2744,7 +2741,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:15:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:15:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -2827,7 +2824,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:12:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:12:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -2864,7 +2861,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:13:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:13:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -3021,7 +3018,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:3 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:3 error: 'workgroupBarrier' must only be called from uniform control flow
   workgroupBarrier();
   ^^^^^^^^^^^^^^^^
 
@@ -3058,7 +3055,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:7:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:7:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -3088,7 +3085,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:7:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:7:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -3123,7 +3120,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:11:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:11:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -3212,7 +3209,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:14:9 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:14:9 error: 'workgroupBarrier' must only be called from uniform control flow
         workgroupBarrier();
         ^^^^^^^^^^^^^^^^
 
@@ -3252,7 +3249,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:19:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:19:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -3318,7 +3315,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:18:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:18:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -3387,7 +3384,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:21:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:21:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -3429,7 +3426,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -3534,7 +3531,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -3564,7 +3561,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -3629,7 +3626,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:11:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:11:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -3657,7 +3654,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:7:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:7:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -3686,7 +3683,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -3718,7 +3715,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:6:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:6:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -3754,7 +3751,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:7:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:7:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -3787,7 +3784,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -3828,7 +3825,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:12:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:12:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -3861,7 +3858,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:6:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:6:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -3902,7 +3899,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:6:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:6:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -3985,7 +3982,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:10:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4023,7 +4020,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:10:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4064,7 +4061,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4112,7 +4109,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:11:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:11:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4154,7 +4151,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4200,7 +4197,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4248,7 +4245,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:10:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4278,7 +4275,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4310,7 +4307,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:11:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:11:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4361,7 +4358,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:11:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:11:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4411,7 +4408,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:12:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:12:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4464,7 +4461,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:12:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:12:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4498,7 +4495,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:13:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:13:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4581,7 +4578,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:21:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:21:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4675,7 +4672,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:18:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:18:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4712,7 +4709,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:16:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:16:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4749,7 +4746,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:16:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:16:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4794,7 +4791,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:24:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:24:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4833,7 +4830,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:18:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:18:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4869,7 +4866,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:7:7 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:7:7 error: 'workgroupBarrier' must only be called from uniform control flow
       workgroupBarrier();
       ^^^^^^^^^^^^^^^^
 
@@ -4905,7 +4902,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:15:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:15:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4942,7 +4939,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:16:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:16:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -4979,7 +4976,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:16:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:16:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5020,7 +5017,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:20:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:20:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5079,7 +5076,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:14:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:14:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5136,7 +5133,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:14:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:14:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5191,7 +5188,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:12:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:12:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5229,7 +5226,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:14:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:14:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5269,7 +5266,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:16:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:16:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5339,7 +5336,7 @@
 
     RunTest(std::move(b), false);
     EXPECT_EQ(error_,
-              R"(warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(error: 'workgroupBarrier' must only be called from uniform control flow
 note: control flow depends on possibly non-uniform value
 note: reading from module-scope private variable 'non_uniform_global' may result in a non-uniform value)");
 }
@@ -5375,7 +5372,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:6:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:6:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5404,7 +5401,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5449,7 +5446,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5482,7 +5479,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5600,7 +5597,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5630,7 +5627,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5663,7 +5660,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5692,7 +5689,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5725,7 +5722,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5829,7 +5826,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5862,7 +5859,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5895,7 +5892,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5929,7 +5926,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:10:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5964,7 +5961,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:10:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -5998,7 +5995,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:10:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6028,7 +6025,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6059,7 +6056,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6091,7 +6088,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:10:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6123,7 +6120,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:10:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6153,7 +6150,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6202,7 +6199,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:10:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6235,7 +6232,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:12:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:12:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6288,7 +6285,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:12:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:12:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6325,7 +6322,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:13:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:13:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6363,7 +6360,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:14:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:14:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6401,7 +6398,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:14:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:14:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6456,7 +6453,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:13:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:13:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6557,7 +6554,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:13:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:13:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6593,7 +6590,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:14:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:14:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6629,7 +6626,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:14:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:14:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6666,7 +6663,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:13:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:13:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6707,7 +6704,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:6:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:6:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6736,7 +6733,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6781,7 +6778,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6812,7 +6809,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6845,7 +6842,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6892,7 +6889,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6925,7 +6922,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6958,7 +6955,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -6991,7 +6988,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:9:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -7025,7 +7022,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:10:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -7060,7 +7057,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:10:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -7094,7 +7091,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:10:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -7240,7 +7237,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:6:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:6:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -7267,7 +7264,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:6:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:6:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -7294,7 +7291,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:6:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:6:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -7324,7 +7321,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -7354,7 +7351,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:8:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -7391,7 +7388,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:5:41 warning: 'dpdx' must only be called from uniform control flow
+              R"(test:5:41 error: 'dpdx' must only be called from uniform control flow
   let b = (non_uniform_global == 0) && (dpdx(1.0) == 0.0);
                                         ^^^^
 
@@ -7538,7 +7535,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:16:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:16:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -7600,7 +7597,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:17:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:17:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -7644,7 +7641,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:21:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:21:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -7714,7 +7711,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:28 warning: possibly non-uniform value passed here
+              R"(test:8:28 error: possibly non-uniform value passed here
   if (workgroupUniformLoad(&data[idx]) > 0) {
                            ^
 
@@ -7746,7 +7743,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:31 warning: possibly non-uniform value passed here
+              R"(test:8:31 error: possibly non-uniform value passed here
   return workgroupUniformLoad(p);
                               ^
 
@@ -7777,7 +7774,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:6:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:6:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -7804,7 +7801,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:6:5 warning: 'storageBarrier' must only be called from uniform control flow
+              R"(test:6:5 error: 'storageBarrier' must only be called from uniform control flow
     storageBarrier();
     ^^^^^^^^^^^^^^
 
@@ -7863,12 +7860,290 @@
 
     RunTest(std::move(b), false);
     EXPECT_EQ(error_,
-              R"(warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(error: 'workgroupBarrier' must only be called from uniform control flow
 note: control flow depends on possibly non-uniform value
 note: reading from module-scope private variable 'v0' may result in a non-uniform value)");
 }
 
 ////////////////////////////////////////////////////////////////////////////////
+/// Tests for the derivative_uniformity diagnostic filter.
+////////////////////////////////////////////////////////////////////////////////
+
+class UniformityAnalysisDiagnosticFilterTest
+    : public UniformityAnalysisTestBase,
+      public ::testing::TestWithParam<ast::DiagnosticSeverity> {
+  protected:
+    // TODO(jrprice): Remove this in favour of utils::ToString() when we change "note" to "info".
+    const char* ToStr(ast::DiagnosticSeverity severity) {
+        switch (severity) {
+            case ast::DiagnosticSeverity::kError:
+                return "error";
+            case ast::DiagnosticSeverity::kWarning:
+                return "warning";
+            case ast::DiagnosticSeverity::kInfo:
+                return "note";
+            default:
+                return "<undefined>";
+        }
+    }
+};
+
+TEST_P(UniformityAnalysisDiagnosticFilterTest, Directive) {
+    auto& param = GetParam();
+    std::ostringstream ss;
+    ss << "diagnostic(" << param << ", derivative_uniformity);"
+       << R"(
+@group(0) @binding(0) var<storage, read_write> non_uniform : i32;
+@group(0) @binding(1) var t : texture_2d<f32>;
+@group(0) @binding(2) var s : sampler;
+
+fn foo() {
+  if (non_uniform == 42) {
+    let color = textureSample(t, s, vec2(0, 0));
+  }
+}
+)";
+
+    RunTest(ss.str(), param != ast::DiagnosticSeverity::kError);
+
+    if (param == ast::DiagnosticSeverity::kOff) {
+        EXPECT_TRUE(error_.empty());
+    } else {
+        std::ostringstream err;
+        err << ToStr(param) << ": 'textureSample' must only be called";
+        EXPECT_THAT(error_, ::testing::HasSubstr(err.str()));
+    }
+}
+
+TEST_P(UniformityAnalysisDiagnosticFilterTest, AttributeOnFunction) {
+    auto& param = GetParam();
+    std::ostringstream ss;
+    ss << R"(
+@group(0) @binding(0) var<storage, read_write> non_uniform : i32;
+@group(0) @binding(1) var t : texture_2d<f32>;
+@group(0) @binding(2) var s : sampler;
+)"
+       << "@diagnostic(" << param << ", derivative_uniformity)"
+       <<
+        R"(fn foo() {
+  if (non_uniform == 42) {
+    let color = textureSample(t, s, vec2(0, 0));
+  }
+}
+)";
+
+    RunTest(ss.str(), param != ast::DiagnosticSeverity::kError);
+    if (param == ast::DiagnosticSeverity::kOff) {
+        EXPECT_TRUE(error_.empty());
+    } else {
+        std::ostringstream err;
+        err << ToStr(param) << ": 'textureSample' must only be called";
+        EXPECT_THAT(error_, ::testing::HasSubstr(err.str()));
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(UniformityAnalysisTest,
+                         UniformityAnalysisDiagnosticFilterTest,
+                         ::testing::Values(ast::DiagnosticSeverity::kError,
+                                           ast::DiagnosticSeverity::kWarning,
+                                           ast::DiagnosticSeverity::kInfo,
+                                           ast::DiagnosticSeverity::kOff));
+
+TEST_F(UniformityAnalysisDiagnosticFilterTest, AttributeOnFunction_CalledByAnotherFunction) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> non_uniform : i32;
+
+@diagnostic(info, derivative_uniformity)
+fn bar() {
+  dpdx(1.0);
+}
+
+fn foo() {
+  if (non_uniform == 42) {
+    bar();
+  }
+}
+)";
+
+    RunTest(src, true);
+    EXPECT_THAT(error_, ::testing::HasSubstr("note: 'dpdx' must only be called"));
+}
+
+TEST_F(UniformityAnalysisDiagnosticFilterTest, AttributeOnFunction_RequirementOnParameter) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> non_uniform : i32;
+
+@diagnostic(info, derivative_uniformity)
+fn bar(x : i32) {
+  if (x == 0) {
+    dpdx(1.0);
+  }
+}
+
+fn foo() {
+  bar(non_uniform);
+}
+)";
+
+    RunTest(src, true);
+    EXPECT_THAT(error_, ::testing::HasSubstr("note: 'dpdx' must only be called"));
+}
+
+TEST_F(UniformityAnalysisDiagnosticFilterTest, AttributeOnFunction_BuiltinInChildCall) {
+    // Make sure that the diagnostic filter does not descend into functions called by the function
+    // with the attribute.
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> non_uniform : i32;
+
+fn bar() {
+  dpdx(1.0);
+}
+
+@diagnostic(off, derivative_uniformity)
+fn foo() {
+  if (non_uniform == 42) {
+    bar();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_THAT(error_, ::testing::HasSubstr(": 'dpdx' must only be called"));
+}
+
+TEST_F(UniformityAnalysisDiagnosticFilterTest, MixOfGlobalAndLocalFilters) {
+    // Test that a global filter is overridden by a local attribute, and that we find multiple
+    // violations until an error is found.
+    std::string src = R"(
+diagnostic(info, derivative_uniformity);
+
+@group(0) @binding(0) var<storage, read_write> non_uniform : i32;
+
+fn a() {
+  if (non_uniform == 42) {
+    dpdx(1.0);
+  }
+}
+
+@diagnostic(off, derivative_uniformity)
+fn b() {
+  if (non_uniform == 42) {
+    dpdx(1.0);
+  }
+}
+
+@diagnostic(info, derivative_uniformity)
+fn c() {
+  if (non_uniform == 42) {
+    dpdx(1.0);
+  }
+}
+
+@diagnostic(warning, derivative_uniformity)
+fn d() {
+  if (non_uniform == 42) {
+    dpdx(1.0);
+  }
+}
+
+@diagnostic(error, derivative_uniformity)
+fn e() {
+  if (non_uniform == 42) {
+    dpdx(1.0);
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:8:5 note: 'dpdx' must only be called from uniform control flow
+    dpdx(1.0);
+    ^^^^
+
+test:7:3 note: control flow depends on possibly non-uniform value
+  if (non_uniform == 42) {
+  ^^
+
+test:7:7 note: reading from read_write storage buffer 'non_uniform' may result in a non-uniform value
+  if (non_uniform == 42) {
+      ^^^^^^^^^^^
+
+test:22:5 note: 'dpdx' must only be called from uniform control flow
+    dpdx(1.0);
+    ^^^^
+
+test:21:3 note: control flow depends on possibly non-uniform value
+  if (non_uniform == 42) {
+  ^^
+
+test:21:7 note: reading from read_write storage buffer 'non_uniform' may result in a non-uniform value
+  if (non_uniform == 42) {
+      ^^^^^^^^^^^
+
+test:29:5 warning: 'dpdx' must only be called from uniform control flow
+    dpdx(1.0);
+    ^^^^
+
+test:28:3 note: control flow depends on possibly non-uniform value
+  if (non_uniform == 42) {
+  ^^
+
+test:28:7 note: reading from read_write storage buffer 'non_uniform' may result in a non-uniform value
+  if (non_uniform == 42) {
+      ^^^^^^^^^^^
+
+test:36:5 error: 'dpdx' must only be called from uniform control flow
+    dpdx(1.0);
+    ^^^^
+
+test:35:3 note: control flow depends on possibly non-uniform value
+  if (non_uniform == 42) {
+  ^^
+
+test:35:7 note: reading from read_write storage buffer 'non_uniform' may result in a non-uniform value
+  if (non_uniform == 42) {
+      ^^^^^^^^^^^
+)");
+}
+
+TEST_F(UniformityAnalysisDiagnosticFilterTest, BarriersNotAffected) {
+    // Make sure that the diagnostic filter does not affect barriers.
+    std::string src = R"(
+diagnostic(off, derivative_uniformity);
+
+@group(0) @binding(0) var<storage, read_write> non_uniform : i32;
+
+fn foo() {
+  if (non_uniform == 42) {
+    dpdx(1.0);
+  }
+}
+
+fn bar() {
+  if (non_uniform == 42) {
+    workgroupBarrier();
+  }
+}
+
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:14:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:13:3 note: control flow depends on possibly non-uniform value
+  if (non_uniform == 42) {
+  ^^
+
+test:13:7 note: reading from read_write storage buffer 'non_uniform' may result in a non-uniform value
+  if (non_uniform == 42) {
+      ^^^^^^^^^^^
+)");
+}
+
+////////////////////////////////////////////////////////////////////////////////
 /// Tests for the quality of the error messages produced by the analysis.
 ////////////////////////////////////////////////////////////////////////////////
 
@@ -7889,7 +8164,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:5:3 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:5:3 error: 'workgroupBarrier' must only be called from uniform control flow
   workgroupBarrier();
   ^^^^^^^^^^^^^^^^
 
@@ -7932,7 +8207,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:5:3 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:5:3 error: 'workgroupBarrier' must only be called from uniform control flow
   workgroupBarrier();
   ^^^^^^^^^^^^^^^^
 
@@ -7975,7 +8250,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:6:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:6:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -8038,7 +8313,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:18:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:18:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
@@ -8073,7 +8348,7 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:6:5 warning: 'workgroupBarrier' must only be called from uniform control flow
+              R"(test:6:5 error: 'workgroupBarrier' must only be called from uniform control flow
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index 18f53fb..57a2e3a 100644
--- a/src/tint/resolver/validator.cc
+++ b/src/tint/resolver/validator.cc
@@ -166,7 +166,13 @@
       sem_(sem),
       enabled_extensions_(enabled_extensions),
       atomic_composite_info_(atomic_composite_info),
-      valid_type_storage_layouts_(valid_type_storage_layouts) {}
+      valid_type_storage_layouts_(valid_type_storage_layouts) {
+    // Set default severities for filterable diagnostic rules.
+    diagnostic_filters_.Set(ast::DiagnosticRule::kDerivativeUniformity,
+                            ast::DiagnosticSeverity::kError);
+    diagnostic_filters_.Set(ast::DiagnosticRule::kChromiumUnreachableCode,
+                            ast::DiagnosticSeverity::kWarning);
+}
 
 Validator::~Validator() = default;
 
@@ -182,6 +188,24 @@
     diagnostics_.add_note(diag::System::Resolver, msg, source);
 }
 
+bool Validator::AddDiagnostic(ast::DiagnosticRule rule,
+                              const std::string& msg,
+                              const Source& source) const {
+    auto severity = diagnostic_filters_.Get(rule);
+    if (severity != ast::DiagnosticSeverity::kOff) {
+        diag::Diagnostic d{};
+        d.severity = ToSeverity(severity);
+        d.system = diag::System::Resolver;
+        d.source = source;
+        d.message = msg;
+        diagnostics_.add(std::move(d));
+        if (severity == ast::DiagnosticSeverity::kError) {
+            return false;
+        }
+    }
+    return true;
+}
+
 // https://gpuweb.github.io/gpuweb/wgsl/#plain-types-section
 bool Validator::IsPlain(const type::Type* type) const {
     return type->is_scalar() ||
@@ -982,7 +1006,8 @@
                          attr->source);
                 return false;
             }
-        } else if (!attr->IsAnyOf<ast::StageAttribute, ast::InternalAttribute>()) {
+        } else if (!attr->IsAnyOf<ast::DiagnosticAttribute, ast::StageAttribute,
+                                  ast::InternalAttribute>()) {
             AddError("attribute is not valid for functions", attr->source);
             return false;
         }
@@ -1358,9 +1383,10 @@
 bool Validator::Statements(utils::VectorRef<const ast::Statement*> stmts) const {
     for (auto* stmt : stmts) {
         if (!sem_.Get(stmt)->IsReachable()) {
-            /// TODO(https://github.com/gpuweb/gpuweb/issues/2378): This may need to
-            /// become an error.
-            AddWarning("code is unreachable", stmt->source);
+            if (!AddDiagnostic(ast::DiagnosticRule::kChromiumUnreachableCode, "code is unreachable",
+                               stmt->source)) {
+                return false;
+            }
             break;
         }
     }
diff --git a/src/tint/resolver/validator.h b/src/tint/resolver/validator.h
index 891f02d..635a6cc 100644
--- a/src/tint/resolver/validator.h
+++ b/src/tint/resolver/validator.h
@@ -22,6 +22,7 @@
 #include "src/tint/ast/pipeline_stage.h"
 #include "src/tint/program_builder.h"
 #include "src/tint/resolver/sem_helper.h"
+#include "src/tint/scope_stack.h"
 #include "src/tint/sem/evaluation_stage.h"
 #include "src/tint/source.h"
 #include "src/tint/utils/hash.h"
@@ -84,6 +85,9 @@
     }
 };
 
+/// DiagnosticFilterStack is a scoped stack of diagnostic filters.
+using DiagnosticFilterStack = ScopeStack<ast::DiagnosticRule, ast::DiagnosticSeverity>;
+
 /// Validation logic for various ast nodes. The validations in general should
 /// be shallow and depend on the resolver to call on children. The validations
 /// also assume that sem changes have already been made. The validation checks
@@ -118,6 +122,18 @@
     /// @param source the note source
     void AddNote(const std::string& msg, const Source& source) const;
 
+    /// Adds the given message to the diagnostics with current severity for the given rule.
+    /// @param rule the diagnostic trigger rule
+    /// @param msg the diagnostic message
+    /// @param source the diagnostic source
+    /// @returns false if the diagnostic is an error for the given trigger rule
+    bool AddDiagnostic(ast::DiagnosticRule rule,
+                       const std::string& msg,
+                       const Source& source) const;
+
+    /// @returns the diagnostic filter stack
+    DiagnosticFilterStack& DiagnosticFilters() { return diagnostic_filters_; }
+
     /// @param type the given type
     /// @returns true if the given type is a plain type
     bool IsPlain(const type::Type* type) const;
@@ -525,6 +541,7 @@
     SymbolTable& symbols_;
     diag::List& diagnostics_;
     SemHelper& sem_;
+    DiagnosticFilterStack diagnostic_filters_;
     const ast::Extensions& enabled_extensions_;
     const utils::Hashmap<const type::Type*, const Source*, 8>& atomic_composite_info_;
     utils::Hashset<TypeAndAddressSpace, 8>& valid_type_storage_layouts_;
diff --git a/src/tint/sem/diagnostic_severity_test.cc b/src/tint/sem/diagnostic_severity_test.cc
new file mode 100644
index 0000000..7ede2bc
--- /dev/null
+++ b/src/tint/sem/diagnostic_severity_test.cc
@@ -0,0 +1,68 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/sem/test_helper.h"
+
+#include "src/tint/sem/module.h"
+
+using namespace tint::number_suffixes;  // NOLINT
+
+namespace tint::sem {
+namespace {
+
+class DiagnosticSeverityTest : public TestHelper {
+  protected:
+    /// Create a program with two functions, setting the severity for "chromium_unreachable_code"
+    /// using an attribute. Test that we correctly track the severity of the filter for the
+    /// functions and the statements with them.
+    /// @param global_severity the global severity of the "chromium_unreachable_code" filter
+    void Run(ast::DiagnosticSeverity global_severity) {
+        // @diagnostic(off, chromium_unreachable_code)
+        // fn foo() {
+        //   return;
+        // }
+        //
+        // fn bar() {
+        //   return;
+        // }
+        auto rule = ast::DiagnosticRule::kChromiumUnreachableCode;
+        auto func_severity = ast::DiagnosticSeverity::kOff;
+
+        auto* return_1 = Return();
+        auto* return_2 = Return();
+        auto* func_attr = DiagnosticAttribute(func_severity, Expr("chromium_unreachable_code"));
+        auto* foo = Func("foo", {}, ty.void_(), utils::Vector{return_1}, utils::Vector{func_attr});
+        auto* bar = Func("bar", {}, ty.void_(), utils::Vector{return_2});
+
+        auto p = Build();
+        EXPECT_TRUE(p.IsValid()) << p.Diagnostics().str();
+
+        EXPECT_EQ(p.Sem().DiagnosticSeverity(foo, rule), func_severity);
+        EXPECT_EQ(p.Sem().DiagnosticSeverity(return_1, rule), func_severity);
+        EXPECT_EQ(p.Sem().DiagnosticSeverity(bar, rule), global_severity);
+        EXPECT_EQ(p.Sem().DiagnosticSeverity(return_2, rule), global_severity);
+    }
+};
+
+TEST_F(DiagnosticSeverityTest, WithDirective) {
+    DiagnosticDirective(ast::DiagnosticSeverity::kError, Expr("chromium_unreachable_code"));
+    Run(ast::DiagnosticSeverity::kError);
+}
+
+TEST_F(DiagnosticSeverityTest, WithoutDirective) {
+    Run(ast::DiagnosticSeverity::kWarning);
+}
+
+}  // namespace
+}  // namespace tint::sem
diff --git a/src/tint/sem/function.h b/src/tint/sem/function.h
index 32468aa..25bacda 100644
--- a/src/tint/sem/function.h
+++ b/src/tint/sem/function.h
@@ -20,6 +20,7 @@
 #include <utility>
 #include <vector>
 
+#include "src/tint/ast/diagnostic_control.h"
 #include "src/tint/ast/variable.h"
 #include "src/tint/sem/call.h"
 #include "src/tint/utils/unique_vector.h"
@@ -256,6 +257,18 @@
     /// @return the location for the return, if provided
     std::optional<uint32_t> ReturnLocation() const { return return_location_; }
 
+    /// Modifies the severity of a specific diagnostic rule for this function.
+    /// @param rule the diagnostic rule
+    /// @param severity the new diagnostic severity
+    void SetDiagnosticSeverity(ast::DiagnosticRule rule, ast::DiagnosticSeverity severity) {
+        diagnostic_severities_[rule] = severity;
+    }
+
+    /// @returns the diagnostic severity modifications applied to this function
+    const ast::DiagnosticRuleSeverities& DiagnosticSeverities() const {
+        return diagnostic_severities_;
+    }
+
   private:
     Function(const Function&) = delete;
     Function(Function&&) = delete;
@@ -276,6 +289,7 @@
     std::vector<const Function*> ancestor_entry_points_;
     const Statement* discard_stmt_ = nullptr;
     sem::Behaviors behaviors_{sem::Behavior::kNext};
+    ast::DiagnosticRuleSeverities diagnostic_severities_;
 
     std::optional<uint32_t> return_location_;
 };
diff --git a/src/tint/sem/info.cc b/src/tint/sem/info.cc
index edeab7e..a3f5b48 100644
--- a/src/tint/sem/info.cc
+++ b/src/tint/sem/info.cc
@@ -14,6 +14,11 @@
 
 #include "src/tint/sem/info.h"
 
+#include "src/tint/sem/expression.h"
+#include "src/tint/sem/function.h"
+#include "src/tint/sem/module.h"
+#include "src/tint/sem/statement.h"
+
 namespace tint::sem {
 
 Info::Info() = default;
@@ -24,4 +29,61 @@
 
 Info& Info::operator=(Info&&) = default;
 
+ast::DiagnosticSeverity Info::DiagnosticSeverity(const ast::Node* ast_node,
+                                                 ast::DiagnosticRule rule) const {
+    // Get the diagnostic severity modification for a node.
+    auto check = [&](auto* node) {
+        auto& severities = node->DiagnosticSeverities();
+        auto itr = severities.find(rule);
+        if (itr != severities.end()) {
+            return itr->second;
+        }
+        return ast::DiagnosticSeverity::kUndefined;
+    };
+
+    // Get the diagnostic severity modification for a function.
+    auto check_func = [&](const sem::Function* func) {
+        auto severity = check(func);
+        if (severity != ast::DiagnosticSeverity::kUndefined) {
+            return severity;
+        }
+
+        // No severity set on the function, so check the module instead.
+        return check(module_);
+    };
+
+    // Get the diagnostic severity modification for a statement.
+    auto check_stmt = [&](const sem::Statement* stmt) {
+        // Walk up the statement hierarchy, checking for diagnostic severity modifications.
+        while (true) {
+            auto severity = check(stmt);
+            if (severity != ast::DiagnosticSeverity::kUndefined) {
+                return severity;
+            }
+            if (!stmt->Parent()) {
+                break;
+            }
+            stmt = stmt->Parent();
+        }
+
+        // No severity set on the statement, so check the function instead.
+        return check_func(stmt->Function());
+    };
+
+    // Query the diagnostic severity from the semantic node that corresponds to the AST node.
+    auto* sem = Get(ast_node);
+    TINT_ASSERT(Resolver, sem != nullptr);
+    auto severity = Switch(
+        sem,  //
+        [&](const sem::Expression* expr) { return check_stmt(expr->Stmt()); },
+        [&](const sem::Statement* stmt) { return check_stmt(stmt); },
+        [&](const sem::Function* func) { return check_func(func); },
+        [&](Default) {
+            // Use the global severity set on the module.
+            return check(module_);
+        });
+    TINT_ASSERT(Resolver, severity != ast::DiagnosticSeverity::kUndefined);
+    return severity;
+}
+
 }  // namespace tint::sem
diff --git a/src/tint/sem/info.h b/src/tint/sem/info.h
index 246510c..c97b014 100644
--- a/src/tint/sem/info.h
+++ b/src/tint/sem/info.h
@@ -20,6 +20,7 @@
 #include <unordered_map>
 #include <vector>
 
+#include "src/tint/ast/diagnostic_control.h"
 #include "src/tint/ast/node.h"
 #include "src/tint/debug.h"
 #include "src/tint/sem/node.h"
@@ -144,6 +145,13 @@
         return &referenced_overrides_.at(from);
     }
 
+    /// Determines the severity of a filterable diagnostic rule for the AST node `ast_node`.
+    /// @param ast_node the AST node
+    /// @param rule the diagnostic rule
+    /// @returns the severity of the rule for that AST node
+    ast::DiagnosticSeverity DiagnosticSeverity(const ast::Node* ast_node,
+                                               ast::DiagnosticRule rule) const;
+
   private:
     // AST node index to semantic node
     std::vector<const CastableBase*> nodes_;
diff --git a/src/tint/sem/module.h b/src/tint/sem/module.h
index 216a2c4..21b08f2 100644
--- a/src/tint/sem/module.h
+++ b/src/tint/sem/module.h
@@ -15,6 +15,7 @@
 #ifndef SRC_TINT_SEM_MODULE_H_
 #define SRC_TINT_SEM_MODULE_H_
 
+#include "src/tint/ast/diagnostic_control.h"
 #include "src/tint/ast/extension.h"
 #include "src/tint/sem/node.h"
 #include "src/tint/utils/vector.h"
@@ -46,9 +47,22 @@
     /// @returns the list of enabled extensions in the module
     const ast::Extensions& Extensions() const { return extensions_; }
 
+    /// Modifies the severity of a specific diagnostic rule for this module.
+    /// @param rule the diagnostic rule
+    /// @param severity the new diagnostic severity
+    void SetDiagnosticSeverity(ast::DiagnosticRule rule, ast::DiagnosticSeverity severity) {
+        diagnostic_severities_[rule] = severity;
+    }
+
+    /// @returns the diagnostic severity modifications applied to this module
+    const ast::DiagnosticRuleSeverities& DiagnosticSeverities() const {
+        return diagnostic_severities_;
+    }
+
   private:
     const utils::Vector<const ast::Node*, 64> dep_ordered_decls_;
     ast::Extensions extensions_;
+    ast::DiagnosticRuleSeverities diagnostic_severities_;
 };
 
 }  // namespace tint::sem
diff --git a/src/tint/sem/statement.h b/src/tint/sem/statement.h
index 09112d5..2d17295 100644
--- a/src/tint/sem/statement.h
+++ b/src/tint/sem/statement.h
@@ -15,6 +15,7 @@
 #ifndef SRC_TINT_SEM_STATEMENT_H_
 #define SRC_TINT_SEM_STATEMENT_H_
 
+#include "src/tint/ast/diagnostic_control.h"
 #include "src/tint/sem/behavior.h"
 #include "src/tint/sem/node.h"
 #include "src/tint/symbol.h"
@@ -109,12 +110,25 @@
     /// according to the behavior analysis
     void SetIsReachable(bool is_reachable) { is_reachable_ = is_reachable; }
 
+    /// Modifies the severity of a specific diagnostic rule for this statement.
+    /// @param rule the diagnostic rule
+    /// @param severity the new diagnostic severity
+    void SetDiagnosticSeverity(ast::DiagnosticRule rule, ast::DiagnosticSeverity severity) {
+        diagnostic_severities_[rule] = severity;
+    }
+
+    /// @returns the diagnostic severity modifications applied to this statement
+    const ast::DiagnosticRuleSeverities& DiagnosticSeverities() const {
+        return diagnostic_severities_;
+    }
+
   private:
     const ast::Statement* const declaration_;
     const CompoundStatement* const parent_;
     const sem::Function* const function_;
     sem::Behaviors behaviors_{sem::Behavior::kNext};
     bool is_reachable_ = true;
+    ast::DiagnosticRuleSeverities diagnostic_severities_;
 };
 
 /// CompoundStatement is the base class of statements that can hold other
diff --git a/src/tint/transform/add_block_attribute_test.cc b/src/tint/transform/add_block_attribute_test.cc
index e4519dd..74dd5a9 100644
--- a/src/tint/transform/add_block_attribute_test.cc
+++ b/src/tint/transform/add_block_attribute_test.cc
@@ -143,7 +143,7 @@
 }
 )";
     auto* expect = R"(
-type Numbers = array<vec4<f32>, 4u>;
+alias Numbers = array<vec4<f32>, 4u>;
 
 @internal(block)
 struct u_block {
@@ -718,13 +718,13 @@
   f : f32,
 }
 
-type MyInner = Inner;
+alias MyInner = Inner;
 
 struct Outer {
   i : MyInner,
 }
 
-type MyOuter = Outer;
+alias MyOuter = Outer;
 
 @internal(block)
 struct u0_block {
@@ -792,7 +792,7 @@
 
 @group(0) @binding(1) var<uniform> u1 : u1_block;
 
-type MyInner = Inner;
+alias MyInner = Inner;
 
 @internal(block)
 struct u0_block {
@@ -801,7 +801,7 @@
 
 @group(0) @binding(0) var<uniform> u0 : u0_block;
 
-type MyOuter = Outer;
+alias MyOuter = Outer;
 
 struct Outer {
   i : MyInner,
diff --git a/src/tint/transform/canonicalize_entry_point_io_test.cc b/src/tint/transform/canonicalize_entry_point_io_test.cc
index 3fc0033..5af8a14 100644
--- a/src/tint/transform/canonicalize_entry_point_io_test.cc
+++ b/src/tint/transform/canonicalize_entry_point_io_test.cc
@@ -173,7 +173,7 @@
 )";
 
     auto* expect = R"(
-type myf32 = f32;
+alias myf32 = f32;
 
 struct tint_symbol_1 {
   @location(1)
@@ -222,7 +222,7 @@
   frag_main_inner(tint_symbol.loc1);
 }
 
-type myf32 = f32;
+alias myf32 = f32;
 )";
 
     DataMap data;
@@ -1589,7 +1589,7 @@
 )";
 
     auto* expect = R"(
-type myf32 = f32;
+alias myf32 = f32;
 
 struct FragmentInput {
   col1 : myf32,
@@ -1601,9 +1601,9 @@
   col2 : myf32,
 }
 
-type MyFragmentInput = FragmentInput;
+alias MyFragmentInput = FragmentInput;
 
-type MyFragmentOutput = FragmentOutput;
+alias MyFragmentOutput = FragmentOutput;
 
 fn foo(x : MyFragmentInput) -> myf32 {
   return x.col1;
@@ -1703,9 +1703,9 @@
   return wrapper_result;
 }
 
-type MyFragmentInput = FragmentInput;
+alias MyFragmentInput = FragmentInput;
 
-type MyFragmentOutput = FragmentOutput;
+alias MyFragmentOutput = FragmentOutput;
 
 fn foo(x : MyFragmentInput) -> myf32 {
   return x.col1;
@@ -1721,7 +1721,7 @@
   col2 : myf32,
 }
 
-type myf32 = f32;
+alias myf32 = f32;
 )";
 
     DataMap data;
diff --git a/src/tint/transform/combine_samplers_test.cc b/src/tint/transform/combine_samplers_test.cc
index cad3109..e400ed5 100644
--- a/src/tint/transform/combine_samplers_test.cc
+++ b/src/tint/transform/combine_samplers_test.cc
@@ -247,7 +247,7 @@
 }
 )";
     auto* expect = R"(
-type Tex2d = texture_2d<f32>;
+alias Tex2d = texture_2d<f32>;
 
 @group(0) @binding(0) @internal(disable_validation__binding_point_collision) var placeholder_sampler : sampler;
 
@@ -297,7 +297,7 @@
   return textureSample(t_s_1, placeholder_sampler, coords);
 }
 
-type Tex2d = texture_2d<f32>;
+alias Tex2d = texture_2d<f32>;
 )";
 
     DataMap data;
diff --git a/src/tint/transform/decompose_memory_access.cc b/src/tint/transform/decompose_memory_access.cc
index cbef564..abb78b2 100644
--- a/src/tint/transform/decompose_memory_access.cc
+++ b/src/tint/transform/decompose_memory_access.cc
@@ -111,8 +111,8 @@
 struct LoadStoreKey {
     type::AddressSpace const address_space;  // buffer address space
     type::Access const access;               // buffer access
-    type::Type const* buf_ty = nullptr;     // buffer type
-    type::Type const* el_ty = nullptr;      // element type
+    type::Type const* buf_ty = nullptr;      // buffer type
+    type::Type const* el_ty = nullptr;       // element type
     bool operator==(const LoadStoreKey& rhs) const {
         return address_space == rhs.address_space && access == rhs.access && buf_ty == rhs.buf_ty &&
                el_ty == rhs.el_ty;
diff --git a/src/tint/transform/decompose_memory_access_test.cc b/src/tint/transform/decompose_memory_access_test.cc
index 4c2cd11..b58d340 100644
--- a/src/tint/transform/decompose_memory_access_test.cc
+++ b/src/tint/transform/decompose_memory_access_test.cc
@@ -3439,9 +3439,9 @@
   c : i32,
 }
 
-type A1 = S1;
+alias A1 = S1;
 
-type A1_Array = array<S1, 3>;
+alias A1_Array = array<S1, 3>;
 
 struct S2 {
   a : i32,
@@ -3449,9 +3449,9 @@
   c : i32,
 }
 
-type A2 = S2;
+alias A2 = S2;
 
-type A2_Array = array<S2>;
+alias A2_Array = array<S2>;
 
 struct SB {
   @size(128)
@@ -3537,9 +3537,9 @@
   b : A2_Array,
 }
 
-type A2_Array = array<S2>;
+alias A2_Array = array<S2>;
 
-type A2 = S2;
+alias A2 = S2;
 
 struct S2 {
   a : i32,
@@ -3547,9 +3547,9 @@
   c : i32,
 }
 
-type A1 = S1;
+alias A1 = S1;
 
-type A1_Array = array<S1, 3>;
+alias A1_Array = array<S1, 3>;
 
 struct S1 {
   a : i32,
diff --git a/src/tint/transform/decompose_strided_array_test.cc b/src/tint/transform/decompose_strided_array_test.cc
index 3befa65..abc01f2 100644
--- a/src/tint/transform/decompose_strided_array_test.cc
+++ b/src/tint/transform/decompose_strided_array_test.cc
@@ -533,7 +533,7 @@
   el : f32,
 }
 
-type ARR = array<strided_arr, 4u>;
+alias ARR = array<strided_arr, 4u>;
 
 struct S {
   a : ARR,
@@ -626,14 +626,14 @@
   el : f32,
 }
 
-type ARR_A = array<strided_arr, 2u>;
+alias ARR_A = array<strided_arr, 2u>;
 
 struct strided_arr_1 {
   @size(128)
   el : array<ARR_A, 3u>,
 }
 
-type ARR_B = array<strided_arr_1, 4u>;
+alias ARR_B = array<strided_arr_1, 4u>;
 
 struct S {
   a : ARR_B,
diff --git a/src/tint/transform/demote_to_helper_test.cc b/src/tint/transform/demote_to_helper_test.cc
index 943da0d..3753360 100644
--- a/src/tint/transform/demote_to_helper_test.cc
+++ b/src/tint/transform/demote_to_helper_test.cc
@@ -542,10 +542,10 @@
 
 @fragment
 fn foo_no_discard(@location(0) in : f32, @location(1) coord : vec2<f32>) {
+  let ret = bar_no_discard(in, coord);
   if (in == 0.0) {
     return;
   }
-  let ret = bar_no_discard(in, coord);
   v2 = ret;
 }
 )";
@@ -591,10 +591,10 @@
 
 @fragment
 fn foo_no_discard(@location(0) in : f32, @location(1) coord : vec2<f32>) {
+  let ret = bar_no_discard(in, coord);
   if ((in == 0.0)) {
     return;
   }
-  let ret = bar_no_discard(in, coord);
   v2 = ret;
 }
 )";
@@ -992,8 +992,9 @@
   }
   var result = 0;
   for (var i = 0; i < 10; i = atomicAdd(&a, 1)) {
-    result += i32(textureSample(t, s, coord).x);
+    result += i;
   }
+  result += i32(textureSample(t, s, coord).x);
   return result;
 }
 )";
@@ -1020,7 +1021,7 @@
         break;
       }
       {
-        result += i32(textureSample(t, s, coord).x);
+        result += i;
       }
 
       continuing {
@@ -1032,6 +1033,7 @@
       }
     }
   }
+  result += i32(textureSample(t, s, coord).x);
   if (tint_discarded) {
     discard;
   }
@@ -1060,8 +1062,9 @@
   var result = 0;
   if (!atomicCompareExchangeWeak(&a, i32(in), 42).exchanged) {
     let xchg = atomicCompareExchangeWeak(&a, i32(in), 42);
-    result = i32(textureSample(t, s, coord).x) * xchg.old_value;
+    result = xchg.old_value;
   }
+  result += i32(textureSample(t, s, coord).x);
   return result;
 }
 )";
@@ -1100,8 +1103,9 @@
       tint_symbol_3.exchanged = tint_symbol_4.exchanged;
     }
     let xchg = tint_symbol_3;
-    result = (i32(textureSample(t, s, coord).x) * xchg.old_value);
+    result = xchg.old_value;
   }
+  result += i32(textureSample(t, s, coord).x);
   if (tint_discarded) {
     discard;
   }
diff --git a/src/tint/transform/direct_variable_access_test.cc b/src/tint/transform/direct_variable_access_test.cc
index 4c112d4..71e7c94 100644
--- a/src/tint/transform/direct_variable_access_test.cc
+++ b/src/tint/transform/direct_variable_access_test.cc
@@ -242,7 +242,7 @@
 
 @group(0) @binding(0) var<uniform> U : array<array<array<vec4<i32>, 8>, 8>, 8>;
 
-type U_X_X_X = array<u32, 3u>;
+alias U_X_X_X = array<u32, 3u>;
 
 fn a_U_X_X_X(pre : i32, p : U_X_X_X, post : i32) -> vec4<i32> {
   return U[p[0]][p[1]][p[2]];
@@ -339,7 +339,7 @@
   return i;
 }
 
-type U_X_X_X = array<u32, 3u>;
+alias U_X_X_X = array<u32, 3u>;
 
 fn a_U_X_X_X(pre : i32, p : U_X_X_X, post : i32) -> vec4<i32> {
   return U[p[0]][p[1]][p[2]];
@@ -429,7 +429,7 @@
   return i;
 }
 
-type U_X_X = array<u32, 2u>;
+alias U_X_X = array<u32, 2u>;
 
 fn a_U_X_X(pre : i32, p : U_X_X, post : i32) -> vec4<i32> {
   return U[p[0]][p[1]];
@@ -515,7 +515,7 @@
   return i;
 }
 
-type U_X_X = array<u32, 2u>;
+alias U_X_X = array<u32, 2u>;
 
 fn a_U_X_X(pre : i32, p : U_X_X, post : i32) -> vec4<i32> {
   return U[p[0]][p[1]];
@@ -605,7 +605,7 @@
   return i;
 }
 
-type U_X_X = array<u32, 2u>;
+alias U_X_X = array<u32, 2u>;
 
 fn a_U_X_X(pre : i32, p : U_X_X, post : i32) -> vec4<i32> {
   return U[p[0]][p[1]];
@@ -695,7 +695,7 @@
   return i;
 }
 
-type U_X_X = array<u32, 2u>;
+alias U_X_X = array<u32, 2u>;
 
 fn a_U_X_X(pre : i32, p : U_X_X, post : i32) -> vec4<i32> {
   return U[p[0]][p[1]];
@@ -793,7 +793,7 @@
 
 @group(0) @binding(0) var<uniform> U : array<vec4<i32>, 8>;
 
-type U_X = array<u32, 1u>;
+alias U_X = array<u32, 1u>;
 
 fn a_U_X(pre : i32, p : U_X, post : i32) -> vec4<i32> {
   return U[p[0]];
@@ -880,7 +880,7 @@
   mat : mat3x4<f32>,
 }
 
-type InnerArr = array<Inner, 4>;
+alias InnerArr = array<Inner, 4>;
 
 struct Outer {
   arr : InnerArr,
@@ -889,19 +889,19 @@
 
 @group(0) @binding(0) var<uniform> U : Outer;
 
-type U_mat_X = array<u32, 1u>;
+alias U_mat_X = array<u32, 1u>;
 
 fn f0_U_mat_X(p : U_mat_X) -> f32 {
   return U.mat[p[0]].x;
 }
 
-type U_arr_X_mat_X = array<u32, 2u>;
+alias U_arr_X_mat_X = array<u32, 2u>;
 
 fn f0_U_arr_X_mat_X(p : U_arr_X_mat_X) -> f32 {
   return U.arr[p[0]].mat[p[0]].x;
 }
 
-type U_arr_X_mat_X_1 = array<u32, 2u>;
+alias U_arr_X_mat_X_1 = array<u32, 2u>;
 
 fn f0_U_arr_X_mat_X_1(p : U_arr_X_mat_X_1) -> f32 {
   return U.arr[p[0]].mat[p[1]].x;
@@ -926,7 +926,7 @@
   return res;
 }
 
-type U_arr_X_mat = array<u32, 1u>;
+alias U_arr_X_mat = array<u32, 1u>;
 
 fn f1_U_arr_X_mat(p : U_arr_X_mat) -> f32 {
   var res : f32;
@@ -947,7 +947,7 @@
   return res;
 }
 
-type U_arr_X = array<u32, 1u>;
+alias U_arr_X = array<u32, 1u>;
 
 fn f2_U_arr_X(p : U_arr_X) -> f32 {
   let p_mat = &(U.arr[p[0]].mat);
@@ -1087,7 +1087,7 @@
 
 @group(0) @binding(0) var<storage, read_write> S : array<vec4<i32>, 8>;
 
-type S_X = array<u32, 1u>;
+alias S_X = array<u32, 1u>;
 
 fn a_S_X(pre : i32, p : S_X, post : i32) {
   S[p[0]] = vec4<i32>();
@@ -1174,7 +1174,7 @@
   mat : mat3x4<f32>,
 }
 
-type InnerArr = array<Inner, 4>;
+alias InnerArr = array<Inner, 4>;
 
 struct Outer {
   arr : InnerArr,
@@ -1183,19 +1183,19 @@
 
 @group(0) @binding(0) var<storage> S : Outer;
 
-type S_mat_X = array<u32, 1u>;
+alias S_mat_X = array<u32, 1u>;
 
 fn f0_S_mat_X(p : S_mat_X) -> f32 {
   return S.mat[p[0]].x;
 }
 
-type S_arr_X_mat_X = array<u32, 2u>;
+alias S_arr_X_mat_X = array<u32, 2u>;
 
 fn f0_S_arr_X_mat_X(p : S_arr_X_mat_X) -> f32 {
   return S.arr[p[0]].mat[p[0]].x;
 }
 
-type S_arr_X_mat_X_1 = array<u32, 2u>;
+alias S_arr_X_mat_X_1 = array<u32, 2u>;
 
 fn f0_S_arr_X_mat_X_1(p : S_arr_X_mat_X_1) -> f32 {
   return S.arr[p[0]].mat[p[1]].x;
@@ -1220,7 +1220,7 @@
   return res;
 }
 
-type S_arr_X_mat = array<u32, 1u>;
+alias S_arr_X_mat = array<u32, 1u>;
 
 fn f1_S_arr_X_mat(p : S_arr_X_mat) -> f32 {
   var res : f32;
@@ -1241,7 +1241,7 @@
   return res;
 }
 
-type S_arr_X = array<u32, 1u>;
+alias S_arr_X = array<u32, 1u>;
 
 fn f2_S_arr_X(p : S_arr_X) -> f32 {
   let p_mat = &(S.arr[p[0]].mat);
@@ -1296,7 +1296,7 @@
 
 var<workgroup> W : array<vec4<i32>, 8>;
 
-type W_X = array<u32, 1u>;
+alias W_X = array<u32, 1u>;
 
 fn a_W_X(pre : i32, p : W_X, post : i32) -> vec4<i32> {
   return W[p[0]];
@@ -1332,7 +1332,7 @@
 
 var<workgroup> W : array<vec4<i32>, 8>;
 
-type W_X = array<u32, 1u>;
+alias W_X = array<u32, 1u>;
 
 fn a_W_X(pre : i32, p : W_X, post : i32) {
   W[p[0]] = vec4<i32>();
@@ -1418,7 +1418,7 @@
   mat : mat3x4<f32>,
 }
 
-type InnerArr = array<Inner, 4>;
+alias InnerArr = array<Inner, 4>;
 
 struct Outer {
   arr : InnerArr,
@@ -1427,19 +1427,19 @@
 
 var<workgroup> W : Outer;
 
-type W_mat_X = array<u32, 1u>;
+alias W_mat_X = array<u32, 1u>;
 
 fn f0_W_mat_X(p : W_mat_X) -> f32 {
   return W.mat[p[0]].x;
 }
 
-type W_arr_X_mat_X = array<u32, 2u>;
+alias W_arr_X_mat_X = array<u32, 2u>;
 
 fn f0_W_arr_X_mat_X(p : W_arr_X_mat_X) -> f32 {
   return W.arr[p[0]].mat[p[0]].x;
 }
 
-type W_arr_X_mat_X_1 = array<u32, 2u>;
+alias W_arr_X_mat_X_1 = array<u32, 2u>;
 
 fn f0_W_arr_X_mat_X_1(p : W_arr_X_mat_X_1) -> f32 {
   return W.arr[p[0]].mat[p[1]].x;
@@ -1464,7 +1464,7 @@
   return res;
 }
 
-type W_arr_X_mat = array<u32, 1u>;
+alias W_arr_X_mat = array<u32, 1u>;
 
 fn f1_W_arr_X_mat(p : W_arr_X_mat) -> f32 {
   var res : f32;
@@ -1485,7 +1485,7 @@
   return res;
 }
 
-type W_arr_X = array<u32, 1u>;
+alias W_arr_X = array<u32, 1u>;
 
 fn f2_W_arr_X(p : W_arr_X) -> f32 {
   let p_mat = &(W.arr[p[0]].mat);
@@ -1762,7 +1762,7 @@
   return (*(p)).i;
 }
 
-type F_X = array<u32, 1u>;
+alias F_X = array<u32, 1u>;
 
 fn a_F_X(pre : i32, p_base : ptr<private, array<i32, 4u>>, p_indices : F_X, post : i32) -> i32 {
   return (*(p_base))[p_indices[0]];
@@ -1774,7 +1774,7 @@
 
 var<private> Pa : array<i32, 4>;
 
-type F_X_1 = array<u32, 1u>;
+alias F_X_1 = array<u32, 1u>;
 
 fn b() {
   a_F(10, &(Pi), 20);
@@ -1890,7 +1890,7 @@
   mat : mat3x4<f32>,
 }
 
-type InnerArr = array<Inner, 4>;
+alias InnerArr = array<Inner, 4>;
 
 struct Outer {
   arr : InnerArr,
@@ -1899,27 +1899,27 @@
 
 var<private> P : Outer;
 
-type F_mat_X = array<u32, 1u>;
+alias F_mat_X = array<u32, 1u>;
 
 fn f0_F_mat_X(p_base : ptr<private, Outer>, p_indices : F_mat_X) -> f32 {
   return (*(p_base)).mat[p_indices[0]].x;
 }
 
-type F_arr_X_mat_X = array<u32, 2u>;
+alias F_arr_X_mat_X = array<u32, 2u>;
 
 fn f0_F_arr_X_mat_X(p_base : ptr<private, Outer>, p_indices : F_arr_X_mat_X) -> f32 {
   return (*(p_base)).arr[p_indices[0]].mat[p_indices[0]].x;
 }
 
-type F_arr_X_mat_X_1 = array<u32, 2u>;
+alias F_arr_X_mat_X_1 = array<u32, 2u>;
 
 fn f0_F_arr_X_mat_X_1(p_base : ptr<private, Outer>, p_indices : F_arr_X_mat_X_1) -> f32 {
   return (*(p_base)).arr[p_indices[0]].mat[p_indices[1]].x;
 }
 
-type F_mat_X_1 = array<u32, 1u>;
+alias F_mat_X_1 = array<u32, 1u>;
 
-type F_arr_X_mat_X_2 = array<u32, 2u>;
+alias F_arr_X_mat_X_2 = array<u32, 2u>;
 
 fn f1_F_mat(p : ptr<private, Outer>) -> f32 {
   var res : f32;
@@ -1940,9 +1940,9 @@
   return res;
 }
 
-type F_arr_X_mat = array<u32, 1u>;
+alias F_arr_X_mat = array<u32, 1u>;
 
-type F_arr_X_mat_X_3 = array<u32, 2u>;
+alias F_arr_X_mat_X_3 = array<u32, 2u>;
 
 fn f1_F_arr_X_mat(p_base : ptr<private, Outer>, p_indices : F_arr_X_mat) -> f32 {
   var res : f32;
@@ -1963,16 +1963,16 @@
   return res;
 }
 
-type F_arr_X = array<u32, 1u>;
+alias F_arr_X = array<u32, 1u>;
 
-type F_arr_X_mat_1 = array<u32, 1u>;
+alias F_arr_X_mat_1 = array<u32, 1u>;
 
 fn f2_F_arr_X(p_base : ptr<private, Outer>, p_indices : F_arr_X) -> f32 {
   let p_mat = &((*(p_base)).arr[p_indices[0]].mat);
   return f1_F_arr_X_mat(p_base, F_arr_X_mat_1(p_indices[0u]));
 }
 
-type F_arr_X_1 = array<u32, 1u>;
+alias F_arr_X_1 = array<u32, 1u>;
 
 fn f3_F_arr_F_mat(p0 : ptr<private, Outer>, p1 : ptr<private, Outer>) -> f32 {
   let p0_inner = &((*(p0)).arr[3]);
@@ -2001,7 +2001,7 @@
   mat : mat3x4<f32>,
 }
 
-type InnerArr = array<Inner, 4>;
+alias InnerArr = array<Inner, 4>;
 
 struct Outer {
   arr : InnerArr,
@@ -2268,13 +2268,13 @@
   return (*(p)).i;
 }
 
-type F_X = array<u32, 1u>;
+alias F_X = array<u32, 1u>;
 
 fn a_F_X(pre : i32, p_base : ptr<function, array<i32, 4u>>, p_indices : F_X, post : i32) -> i32 {
   return (*(p_base))[p_indices[0]];
 }
 
-type F_X_1 = array<u32, 1u>;
+alias F_X_1 = array<u32, 1u>;
 
 fn b() {
   var Fi : i32;
@@ -2460,13 +2460,13 @@
   return U_str.i;
 }
 
-type U_arr_X = array<u32, 1u>;
+alias U_arr_X = array<u32, 1u>;
 
 fn fn_u_U_arr_X(p : U_arr_X) -> vec4<i32> {
   return U_arr[p[0]];
 }
 
-type U_arr_arr_X_X = array<u32, 2u>;
+alias U_arr_arr_X_X = array<u32, 2u>;
 
 fn fn_u_U_arr_arr_X_X(p : U_arr_arr_X_X) -> vec4<i32> {
   return U_arr_arr[p[0]][p[1]];
@@ -2480,13 +2480,13 @@
   return S_str.i;
 }
 
-type S_arr_X = array<u32, 1u>;
+alias S_arr_X = array<u32, 1u>;
 
 fn fn_s_S_arr_X(p : S_arr_X) -> vec4<i32> {
   return S_arr[p[0]];
 }
 
-type S_arr_arr_X_X = array<u32, 2u>;
+alias S_arr_arr_X_X = array<u32, 2u>;
 
 fn fn_s_S_arr_arr_X_X(p : S_arr_arr_X_X) -> vec4<i32> {
   return S_arr_arr[p[0]][p[1]];
@@ -2500,13 +2500,13 @@
   return W_str.i;
 }
 
-type W_arr_X = array<u32, 1u>;
+alias W_arr_X = array<u32, 1u>;
 
 fn fn_w_W_arr_X(p : W_arr_X) -> vec4<i32> {
   return W_arr[p[0]];
 }
 
-type W_arr_arr_X_X = array<u32, 2u>;
+alias W_arr_arr_X_X = array<u32, 2u>;
 
 fn fn_w_W_arr_arr_X_X(p : W_arr_arr_X_X) -> vec4<i32> {
   return W_arr_arr[p[0]][p[1]];
@@ -2578,7 +2578,7 @@
   return i;
 }
 
-type S_X = array<u32, 1u>;
+alias S_X = array<u32, 1u>;
 
 fn b_S_X(p : S_X) -> i32 {
   return S[p[0]][a(S[p[0]][0][1][2])][a(S[p[0]][a(3)][4][5])][a(S[p[0]][6][a(7)][8])];
@@ -2620,13 +2620,13 @@
 
 @group(0) @binding(0) var<storage> S : array<array<array<array<i32, 9>, 9>, 9>, 50>;
 
-type S_X_X_X_X = array<u32, 4u>;
+alias S_X_X_X_X = array<u32, 4u>;
 
 fn a_S_X_X_X_X(pre : i32, i : S_X_X_X_X, post : i32) -> i32 {
   return S[i[0]][i[0]][i[1]][i[2]];
 }
 
-type S_X = array<u32, 1u>;
+alias S_X = array<u32, 1u>;
 
 fn b_S_X(p : S_X) -> i32 {
   return a_S_X_X_X_X(10, S_X_X_X_X(p[0u], u32(a_S_X_X_X_X(20, S_X_X_X_X(p[0u], 0, 1, 2), 30)), u32(a_S_X_X_X_X(40, S_X_X_X_X(p[0u], 3, 4, 5), 50)), u32(a_S_X_X_X_X(60, S_X_X_X_X(p[0u], 6, 7, 8), 70))), 80);
@@ -2673,9 +2673,9 @@
   return i;
 }
 
-type S_X = array<u32, 1u>;
+alias S_X = array<u32, 1u>;
 
-type U_X = array<u32, 1u>;
+alias U_X = array<u32, 1u>;
 
 fn b_S_X_U_X(s : S_X, u : U_X) -> i32 {
   return S[s[0]][a(U[u[0]][0][1].x)][a(U[u[0]][a(3)][4].y)];
diff --git a/src/tint/transform/module_scope_var_to_entry_point_param_test.cc b/src/tint/transform/module_scope_var_to_entry_point_param_test.cc
index 7ae15be..999761d 100644
--- a/src/tint/transform/module_scope_var_to_entry_point_param_test.cc
+++ b/src/tint/transform/module_scope_var_to_entry_point_param_test.cc
@@ -621,7 +621,7 @@
   arr : array<f32>,
 }
 
-type myarray = array<f32>;
+alias myarray = array<f32>;
 
 @compute @workgroup_size(1)
 fn main(@group(0) @binding(0) @internal(disable_validation__entry_point_parameter) @internal(disable_validation__ignore_address_space) tint_symbol : ptr<storage, tint_symbol_1>) {
@@ -656,7 +656,7 @@
   _ = (*(tint_symbol)).arr[0];
 }
 
-type myarray = array<f32>;
+alias myarray = array<f32>;
 )";
 
     auto got = Run<ModuleScopeVarToEntryPointParam>(src);
diff --git a/src/tint/transform/multiplanar_external_texture_test.cc b/src/tint/transform/multiplanar_external_texture_test.cc
index c76e3e3..af1d0b4 100644
--- a/src/tint/transform/multiplanar_external_texture_test.cc
+++ b/src/tint/transform/multiplanar_external_texture_test.cc
@@ -1657,7 +1657,7 @@
 
 @group(0) @binding(3) var<uniform> ext_tex_params : ExternalTextureParams;
 
-type ET = texture_external;
+alias ET = texture_external;
 
 fn gammaCorrection(v : vec3<f32>, params : GammaTransferParams) -> vec3<f32> {
   let cond = (abs(v) < vec3<f32>(params.D));
@@ -1795,7 +1795,7 @@
 
 @group(0) @binding(1) var smp : sampler;
 
-type ET = texture_external;
+alias ET = texture_external;
 )";
     DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(MultiplanarExternalTexture::BindingsMap{
diff --git a/src/tint/transform/preserve_padding_test.cc b/src/tint/transform/preserve_padding_test.cc
index f47cb3e..854b4a5 100644
--- a/src/tint/transform/preserve_padding_test.cc
+++ b/src/tint/transform/preserve_padding_test.cc
@@ -386,7 +386,7 @@
     auto* expect = R"(
 enable chromium_experimental_full_ptr_parameters;
 
-type Array = array<array<vec3<u32>, 4>, 3>;
+alias Array = array<array<vec3<u32>, 4>, 3>;
 
 @group(0) @binding(0) var<storage, read_write> v : Array;
 
diff --git a/src/tint/transform/promote_initializers_to_let_test.cc b/src/tint/transform/promote_initializers_to_let_test.cc
index dd72671..32e2ec9 100644
--- a/src/tint/transform/promote_initializers_to_let_test.cc
+++ b/src/tint/transform/promote_initializers_to_let_test.cc
@@ -1194,7 +1194,7 @@
 
 TEST_F(PromoteInitializersToLetTest, NoChangeOnVarDecl) {
     auto* src = R"(
-type F = f32;
+alias F = f32;
 
 fn f() {
   var local_arr = array<f32, 4u>(0.0, 1.0, 2.0, 3.0);
@@ -1222,7 +1222,7 @@
 
 const module_str : F = F(2.0);
 
-type F = f32;
+alias F = f32;
 
 const module_arr : array<f32, 4u> = array<f32, 4u>(0.0, 1.0, 2.0, 3.0);
 )";
diff --git a/src/tint/transform/renamer_test.cc b/src/tint/transform/renamer_test.cc
index 0628063..152489a 100644
--- a/src/tint/transform/renamer_test.cc
+++ b/src/tint/transform/renamer_test.cc
@@ -1673,7 +1673,7 @@
 )");
 
     auto expect = expand(R"(
-type tint_symbol = i32;
+alias tint_symbol = i32;
 
 @fragment
 fn tint_symbol_1() {
diff --git a/src/tint/transform/single_entry_point_test.cc b/src/tint/transform/single_entry_point_test.cc
index b225008..b8f79f6 100644
--- a/src/tint/transform/single_entry_point_test.cc
+++ b/src/tint/transform/single_entry_point_test.cc
@@ -389,7 +389,7 @@
 
 @id(5) override c5 : u32 = (2 * c4);
 
-type arr_ty = array<i32, (2 * c5)>;
+alias arr_ty = array<i32, (2 * c5)>;
 
 var<workgroup> arr : arr_ty;
 
@@ -593,7 +593,7 @@
     auto* src = R"(
 const MY_SIZE = 5u;
 
-type Arr = array<i32, MY_SIZE>;
+alias Arr = array<i32, MY_SIZE>;
 
 @fragment
 fn main() {
diff --git a/src/tint/transform/spirv_atomic_test.cc b/src/tint/transform/spirv_atomic_test.cc
index 36312f7..bef061c 100644
--- a/src/tint/transform/spirv_atomic_test.cc
+++ b/src/tint/transform/spirv_atomic_test.cc
@@ -226,13 +226,13 @@
 )";
 
     auto* expect = R"(
-type A0 = u32;
+alias A0 = u32;
 
-type A1 = array<A0, 1>;
+alias A1 = array<A0, 1>;
 
-type A2 = array<A1, 2>;
+alias A2 = array<A1, 2>;
 
-type A3 = array<A2, 3>;
+alias A3 = array<A2, 3>;
 
 var<workgroup> wg : array<array<array<atomic<u32>, 1u>, 2u>, 3u>;
 
diff --git a/src/tint/transform/unshadow_test.cc b/src/tint/transform/unshadow_test.cc
index d2ee8a5..ec17ff5 100644
--- a/src/tint/transform/unshadow_test.cc
+++ b/src/tint/transform/unshadow_test.cc
@@ -66,7 +66,7 @@
 )";
 
     auto* expect = R"(
-type a = i32;
+alias a = i32;
 
 fn X() {
   var a_1 = false;
@@ -116,7 +116,7 @@
   const a_3 = true;
 }
 
-type a = i32;
+alias a = i32;
 )";
 
     auto got = Run<Unshadow>(src);
@@ -704,7 +704,7 @@
 )";
 
     auto* expect = R"(
-type a = i32;
+alias a = i32;
 
 fn F(a_1 : a) {
   {
@@ -745,7 +745,7 @@
   }
 }
 
-type a = i32;
+alias a = i32;
 )";
 
     auto got = Run<Unshadow>(src);
diff --git a/src/tint/utils/math.h b/src/tint/utils/math.h
index 27dd232..8bbbcc8 100644
--- a/src/tint/utils/math.h
+++ b/src/tint/utils/math.h
@@ -66,10 +66,10 @@
 #endif
 
     // Non intrinsic (slow) path. Supports constexpr evaluation.
-    for (size_t clz = 0; clz < 64; clz++) {
-        size_t bit = 63 - clz;
-        if (value & (static_cast<size_t>(1u) << bit)) {
-            return bit;
+    for (uint64_t clz = 0; clz < 64; clz++) {
+        uint64_t bit = 63 - clz;
+        if (value & (static_cast<uint64_t>(1u) << bit)) {
+            return static_cast<uint32_t>(bit);
         }
     }
     return 64;
diff --git a/src/tint/writer/glsl/generator_impl.cc b/src/tint/writer/glsl/generator_impl.cc
index 8bd4033..df49694 100644
--- a/src/tint/writer/glsl/generator_impl.cc
+++ b/src/tint/writer/glsl/generator_impl.cc
@@ -250,7 +250,7 @@
 
     auto* mod = builder_.Sem().Module();
     for (auto* decl : mod->DependencyOrderedDeclarations()) {
-        if (decl->IsAnyOf<ast::Alias, ast::StaticAssert>()) {
+        if (decl->IsAnyOf<ast::Alias, ast::ConstAssert, ast::DiagnosticControl>()) {
             continue;  // These are not emitted.
         }
 
@@ -2796,7 +2796,7 @@
                     return false;
                 });
         },
-        [&](const ast::StaticAssert*) {
+        [&](const ast::ConstAssert*) {
             return true;  // Not emitted
         },
         [&](Default) {
diff --git a/src/tint/writer/hlsl/generator_impl.cc b/src/tint/writer/hlsl/generator_impl.cc
index 82237db..bde9d70 100644
--- a/src/tint/writer/hlsl/generator_impl.cc
+++ b/src/tint/writer/hlsl/generator_impl.cc
@@ -310,7 +310,7 @@
 
     auto* mod = builder_.Sem().Module();
     for (auto* decl : mod->DependencyOrderedDeclarations()) {
-        if (decl->IsAnyOf<ast::Alias, ast::Enable, ast::StaticAssert>()) {
+        if (decl->IsAnyOf<ast::Alias, ast::DiagnosticControl, ast::Enable, ast::ConstAssert>()) {
             continue;  // These are not emitted.
         }
 
@@ -3842,7 +3842,7 @@
                     return false;
                 });
         },
-        [&](const ast::StaticAssert*) {
+        [&](const ast::ConstAssert*) {
             return true;  // Not emitted
         },
         [&](Default) {  //
diff --git a/src/tint/writer/hlsl/generator_impl_static_assert_test.cc b/src/tint/writer/hlsl/generator_impl_const_assert_test.cc
similarity index 78%
rename from src/tint/writer/hlsl/generator_impl_static_assert_test.cc
rename to src/tint/writer/hlsl/generator_impl_const_assert_test.cc
index 8ae5dd8..eac90c6 100644
--- a/src/tint/writer/hlsl/generator_impl_static_assert_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_const_assert_test.cc
@@ -21,23 +21,23 @@
 
 using HlslGeneratorImplTest = TestHelper;
 
-TEST_F(HlslGeneratorImplTest, Emit_GlobalStaticAssert) {
-    GlobalStaticAssert(true);
+TEST_F(HlslGeneratorImplTest, Emit_GlobalConstAssert) {
+    GlobalConstAssert(true);
 
     GeneratorImpl& gen = Build();
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
-    // static asserts are not emitted
+    // const asserts are not emitted
     EXPECT_EQ(gen.result(), "");
 }
 
-TEST_F(HlslGeneratorImplTest, Emit_FunctionStaticAssert) {
-    Func("f", utils::Empty, ty.void_(), utils::Vector{StaticAssert(true)});
+TEST_F(HlslGeneratorImplTest, Emit_FunctionConstAssert) {
+    Func("f", utils::Empty, ty.void_(), utils::Vector{ConstAssert(true)});
 
     GeneratorImpl& gen = Build();
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
-    // static asserts are not emitted
+    // const asserts are not emitted
     EXPECT_EQ(gen.result(), R"(void f() {
 }
 )");
diff --git a/src/tint/writer/msl/generator_impl.cc b/src/tint/writer/msl/generator_impl.cc
index 251483e..29ce69f 100644
--- a/src/tint/writer/msl/generator_impl.cc
+++ b/src/tint/writer/msl/generator_impl.cc
@@ -313,11 +313,15 @@
                 }
                 return EmitFunction(func);
             },
+            [&](const ast::DiagnosticControl*) {
+                // Do nothing for diagnostic directives in MSL
+                return true;
+            },
             [&](const ast::Enable*) {
                 // Do nothing for enabling extension in MSL
                 return true;
             },
-            [&](const ast::StaticAssert*) {
+            [&](const ast::ConstAssert*) {
                 return true;  // Not emitted
             },
             [&](Default) {
@@ -2469,7 +2473,7 @@
                     return false;
                 });
         },
-        [&](const ast::StaticAssert*) {
+        [&](const ast::ConstAssert*) {
             return true;  // Not emitted
         },
         [&](Default) {
diff --git a/src/tint/writer/msl/generator_impl_static_assert_test.cc b/src/tint/writer/msl/generator_impl_const_assert_test.cc
similarity index 79%
rename from src/tint/writer/msl/generator_impl_static_assert_test.cc
rename to src/tint/writer/msl/generator_impl_const_assert_test.cc
index 5b8ca3f..a9850eb 100644
--- a/src/tint/writer/msl/generator_impl_static_assert_test.cc
+++ b/src/tint/writer/msl/generator_impl_const_assert_test.cc
@@ -21,26 +21,26 @@
 
 using MslGeneratorImplTest = TestHelper;
 
-TEST_F(MslGeneratorImplTest, Emit_GlobalStaticAssert) {
-    GlobalStaticAssert(true);
+TEST_F(MslGeneratorImplTest, Emit_GlobalConstAssert) {
+    GlobalConstAssert(true);
 
     GeneratorImpl& gen = Build();
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
-    // static asserts are not emitted
+    // const asserts are not emitted
     EXPECT_EQ(gen.result(), R"(#include <metal_stdlib>
 
 using namespace metal;
 )");
 }
 
-TEST_F(MslGeneratorImplTest, Emit_FunctionStaticAssert) {
-    Func("f", utils::Empty, ty.void_(), utils::Vector{StaticAssert(true)});
+TEST_F(MslGeneratorImplTest, Emit_FunctionConstAssert) {
+    Func("f", utils::Empty, ty.void_(), utils::Vector{ConstAssert(true)});
 
     GeneratorImpl& gen = Build();
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
-    // static asserts are not emitted
+    // const asserts are not emitted
     EXPECT_EQ(gen.result(), R"(#include <metal_stdlib>
 
 using namespace metal;
diff --git a/src/tint/writer/spirv/builder.cc b/src/tint/writer/spirv/builder.cc
index 9df433b..39297ae 100644
--- a/src/tint/writer/spirv/builder.cc
+++ b/src/tint/writer/spirv/builder.cc
@@ -3614,7 +3614,7 @@
         [&](const ast::ReturnStatement* r) { return GenerateReturnStatement(r); },
         [&](const ast::SwitchStatement* s) { return GenerateSwitchStatement(s); },
         [&](const ast::VariableDeclStatement* v) { return GenerateVariableDeclStatement(v); },
-        [&](const ast::StaticAssert*) {
+        [&](const ast::ConstAssert*) {
             return true;  // Not emitted
         },
         [&](Default) {
diff --git a/src/tint/writer/spirv/builder_static_assert_test.cc b/src/tint/writer/spirv/builder_const_assert_test.cc
similarity index 82%
rename from src/tint/writer/spirv/builder_static_assert_test.cc
rename to src/tint/writer/spirv/builder_const_assert_test.cc
index 6e5092d..bca6deb 100644
--- a/src/tint/writer/spirv/builder_static_assert_test.cc
+++ b/src/tint/writer/spirv/builder_const_assert_test.cc
@@ -22,26 +22,26 @@
 
 using BuilderTest = TestHelper;
 
-TEST_F(BuilderTest, GlobalStaticAssert) {
-    GlobalStaticAssert(true);
+TEST_F(BuilderTest, GlobalConstAssert) {
+    GlobalConstAssert(true);
 
     spirv::Builder& b = Build();
 
     ASSERT_TRUE(b.Build()) << b.error();
 
-    // static asserts are not emitted
+    // const asserts are not emitted
     EXPECT_EQ(DumpInstructions(b.types()), "");
     EXPECT_EQ(b.functions().size(), 0u);
 }
 
-TEST_F(BuilderTest, FunctionStaticAssert) {
-    Func("f", utils::Empty, ty.void_(), utils::Vector{StaticAssert(true)});
+TEST_F(BuilderTest, FunctionConstAssert) {
+    Func("f", utils::Empty, ty.void_(), utils::Vector{ConstAssert(true)});
 
     spirv::Builder& b = Build();
 
     ASSERT_TRUE(b.Build()) << b.error();
 
-    // static asserts are not emitted
+    // const asserts are not emitted
     EXPECT_EQ(DumpInstructions(b.types()), R"(%2 = OpTypeVoid
 %1 = OpTypeFunction %2
 )");
diff --git a/src/tint/writer/wgsl/generator_impl.cc b/src/tint/writer/wgsl/generator_impl.cc
index 0c20af7..bb91d93 100644
--- a/src/tint/writer/wgsl/generator_impl.cc
+++ b/src/tint/writer/wgsl/generator_impl.cc
@@ -63,18 +63,28 @@
 GeneratorImpl::~GeneratorImpl() = default;
 
 bool GeneratorImpl::Generate() {
-    // Generate enable directives before any other global declarations.
+    // Generate directives before any other global declarations.
+    bool has_directives = false;
     for (auto enable : program_->AST().Enables()) {
         if (!EmitEnable(enable)) {
             return false;
         }
+        has_directives = true;
     }
-    if (!program_->AST().Enables().IsEmpty()) {
+    for (auto diagnostic : program_->AST().DiagnosticControls()) {
+        auto out = line();
+        if (!EmitDiagnosticControl(out, diagnostic)) {
+            return false;
+        }
+        out << ";";
+        has_directives = true;
+    }
+    if (has_directives) {
         line();
     }
     // Generate global declarations in the order they appear in the module.
     for (auto* decl : program_->AST().GlobalDeclarations()) {
-        if (decl->Is<ast::Enable>()) {
+        if (decl->IsAnyOf<ast::DiagnosticControl, ast::Enable>()) {
             continue;
         }
         if (!Switch(
@@ -82,7 +92,7 @@
                 [&](const ast::TypeDecl* td) { return EmitTypeDecl(td); },
                 [&](const ast::Function* func) { return EmitFunction(func); },
                 [&](const ast::Variable* var) { return EmitVariable(line(), var); },
-                [&](const ast::StaticAssert* sa) { return EmitStaticAssert(sa); },
+                [&](const ast::ConstAssert* ca) { return EmitConstAssert(ca); },
                 [&](Default) {
                     TINT_UNREACHABLE(Writer, diagnostics_);
                     return false;
@@ -97,6 +107,13 @@
     return true;
 }
 
+bool GeneratorImpl::EmitDiagnosticControl(std::ostream& out,
+                                          const ast::DiagnosticControl* diagnostic) {
+    out << "diagnostic(" << diagnostic->severity << ", "
+        << program_->Symbols().NameFor(diagnostic->rule_name->symbol) << ")";
+    return true;
+}
+
 bool GeneratorImpl::EmitEnable(const ast::Enable* enable) {
     auto out = line();
     out << "enable " << enable->extension << ";";
@@ -108,7 +125,7 @@
         ty,
         [&](const ast::Alias* alias) {  //
             auto out = line();
-            out << "type " << program_->Symbols().NameFor(alias->name) << " = ";
+            out << "alias " << program_->Symbols().NameFor(alias->name) << " = ";
             if (!EmitType(out, alias->type)) {
                 return false;
             }
@@ -780,6 +797,9 @@
                 out << "builtin(" << builtin->builtin << ")";
                 return true;
             },
+            [&](const ast::DiagnosticAttribute* diagnostic) {
+                return EmitDiagnosticControl(out, diagnostic->control);
+            },
             [&](const ast::InterpolateAttribute* interpolate) {
                 out << "interpolate(" << interpolate->type;
                 if (interpolate->sampling != ast::InterpolationSampling::kUndefined) {
@@ -992,7 +1012,7 @@
         [&](const ast::ForLoopStatement* l) { return EmitForLoop(l); },
         [&](const ast::WhileStatement* l) { return EmitWhile(l); },
         [&](const ast::ReturnStatement* r) { return EmitReturn(r); },
-        [&](const ast::StaticAssert* s) { return EmitStaticAssert(s); },
+        [&](const ast::ConstAssert* c) { return EmitConstAssert(c); },
         [&](const ast::SwitchStatement* s) { return EmitSwitch(s); },
         [&](const ast::VariableDeclStatement* v) { return EmitVariable(line(), v->variable); },
         [&](Default) {
@@ -1299,7 +1319,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitStaticAssert(const ast::StaticAssert* stmt) {
+bool GeneratorImpl::EmitConstAssert(const ast::ConstAssert* stmt) {
     auto out = line();
     out << "static_assert ";
     if (!EmitExpression(out, stmt->condition)) {
diff --git a/src/tint/writer/wgsl/generator_impl.h b/src/tint/writer/wgsl/generator_impl.h
index f314efb..87a1b54 100644
--- a/src/tint/writer/wgsl/generator_impl.h
+++ b/src/tint/writer/wgsl/generator_impl.h
@@ -52,7 +52,12 @@
     /// @returns true on successful generation; false otherwise
     bool Generate();
 
-    /// Handles generating a enable directive
+    /// Handles generating a diagnostic control
+    /// @param out the output of the expression stream
+    /// @param diagnostic the diagnostic control node
+    /// @returns true if the diagnostic control was emitted
+    bool EmitDiagnosticControl(std::ostream& out, const ast::DiagnosticControl* diagnostic);
+    /// Handles generating an enable directive
     /// @param enable the enable node
     /// @returns true if the enable directive was emitted
     bool EmitEnable(const ast::Enable* enable);
@@ -165,10 +170,10 @@
     /// @param stmt the statement to emit
     /// @returns true if the statement was successfully emitted
     bool EmitReturn(const ast::ReturnStatement* stmt);
-    /// Handles static assertion statements
+    /// Handles const assertion statements
     /// @param stmt the statement to emit
     /// @returns true if the statement was successfully emitted
-    bool EmitStaticAssert(const ast::StaticAssert* stmt);
+    bool EmitConstAssert(const ast::ConstAssert* stmt);
     /// Handles statement
     /// @param stmt the statement to emit
     /// @returns true if the statement was emitted
diff --git a/src/tint/writer/wgsl/generator_impl_alias_type_test.cc b/src/tint/writer/wgsl/generator_impl_alias_type_test.cc
index dd0584d..10a56b1 100644
--- a/src/tint/writer/wgsl/generator_impl_alias_type_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_alias_type_test.cc
@@ -25,7 +25,7 @@
     GeneratorImpl& gen = Build();
 
     ASSERT_TRUE(gen.EmitTypeDecl(alias)) << gen.error();
-    EXPECT_EQ(gen.result(), R"(type a = f32;
+    EXPECT_EQ(gen.result(), R"(alias a = f32;
 )");
 }
 
@@ -45,7 +45,7 @@
   a : f32,
   b : i32,
 }
-type B = A;
+alias B = A;
 )");
 }
 
@@ -60,7 +60,7 @@
     GeneratorImpl& gen = Build();
 
     ASSERT_TRUE(gen.EmitTypeDecl(alias)) << gen.error();
-    EXPECT_EQ(gen.result(), R"(type B = A;
+    EXPECT_EQ(gen.result(), R"(alias B = A;
 )");
 }
 
diff --git a/src/tint/writer/wgsl/generator_impl_static_assert_test.cc b/src/tint/writer/wgsl/generator_impl_const_assert_test.cc
similarity index 83%
rename from src/tint/writer/wgsl/generator_impl_static_assert_test.cc
rename to src/tint/writer/wgsl/generator_impl_const_assert_test.cc
index 9e7a1c0..9f76d65 100644
--- a/src/tint/writer/wgsl/generator_impl_static_assert_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_const_assert_test.cc
@@ -21,8 +21,8 @@
 
 using WgslGeneratorImplTest = TestHelper;
 
-TEST_F(WgslGeneratorImplTest, Emit_GlobalStaticAssert) {
-    GlobalStaticAssert(true);
+TEST_F(WgslGeneratorImplTest, Emit_GlobalConstAssert) {
+    GlobalConstAssert(true);
 
     GeneratorImpl& gen = Build();
 
@@ -31,8 +31,8 @@
 )");
 }
 
-TEST_F(WgslGeneratorImplTest, Emit_FunctionStaticAssert) {
-    Func("f", utils::Empty, ty.void_(), utils::Vector{StaticAssert(true)});
+TEST_F(WgslGeneratorImplTest, Emit_FunctionConstAssert) {
+    Func("f", utils::Empty, ty.void_(), utils::Vector{ConstAssert(true)});
 
     GeneratorImpl& gen = Build();
 
diff --git a/src/tint/writer/wgsl/generator_impl_diagnostic_test.cc b/src/tint/writer/wgsl/generator_impl_diagnostic_test.cc
new file mode 100644
index 0000000..d9ce01c
--- /dev/null
+++ b/src/tint/writer/wgsl/generator_impl_diagnostic_test.cc
@@ -0,0 +1,48 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/writer/wgsl/test_helper.h"
+
+namespace tint::writer::wgsl {
+namespace {
+
+using WgslGeneratorImplTest = TestHelper;
+
+TEST_F(WgslGeneratorImplTest, Emit_DiagnosticDirective) {
+    DiagnosticDirective(ast::DiagnosticSeverity::kError, Expr("chromium_unreachable_code"));
+
+    GeneratorImpl& gen = Build();
+
+    ASSERT_TRUE(gen.Generate());
+    EXPECT_EQ(gen.result(), R"(diagnostic(error, chromium_unreachable_code);
+
+)");
+}
+
+TEST_F(WgslGeneratorImplTest, Emit_DiagnosticAttribute) {
+    auto* attr =
+        DiagnosticAttribute(ast::DiagnosticSeverity::kError, Expr("chromium_unreachable_code"));
+    Func("foo", {}, ty.void_(), {}, utils::Vector{attr});
+
+    GeneratorImpl& gen = Build();
+
+    ASSERT_TRUE(gen.Generate());
+    EXPECT_EQ(gen.result(), R"(@diagnostic(error, chromium_unreachable_code)
+fn foo() {
+}
+)");
+}
+
+}  // namespace
+}  // namespace tint::writer::wgsl