Import Tint changes from Dawn

Changes:
  - 558f0dfed76840e3793d2468b3b504d465b8ee89 [tint][fuzzers] Port ast_printer_fuzz to tint_wgsl_fuzzer by Ben Clayton <bclayton@google.com>
  - 0619b8c5f6712425ba382aeca9465a607b8a5559 [tint][fuzzers] Port tint_ast_clone_fuzzer to tint_wgsl_f... by Ben Clayton <bclayton@google.com>
  - d2170a3da19e5108c717198a4cfd00a94e8b0ec8 [spirv-reader] Add requires for RW textures by James Price <jrprice@google.com>
  - cdcc15311ff58993d66ac4923ff9e259c7ea3c9f [tint] Remove unused include from common options by James Price <jrprice@google.com>
  - 631aaa36b20883f72f2a428e3fe9912d1634ec4f [glsl][hlsl][msl][spirv] Ignore requires directive by James Price <jrprice@google.com>
  - 0840920e4a29384371c9c1624970815d901c0a7c [tint] Handle Requires in SingleEntryPoint by James Price <jrprice@google.com>
  - b51477ba2f34044f5422c9aa40df4519e3b51bed [wgsl] Parse RW storage texture language feature by James Price <jrprice@google.com>
  - bdbeb35882c4a8352c6119b5493c88ead4f7aaea [wgsl][writer] Emit requires directives by James Price <jrprice@google.com>
  - 9b6ba18fcea05507c72b2b8b2994c017f0fb66da [tint][ir] Handle requires directives by James Price <jrprice@google.com>
  - 254e3a9e8ff75b95451b44e8fbddcb828082a6f9 [tint] Resolve requires directives by James Price <jrprice@google.com>
  - 55b671bf57c5f5ea1374411ed793a642221f6bee [tint][wgsl] Add `Requires` AST node by James Price <jrprice@google.com>
  - b926b1f351c73118101c30cf6b7de3dfa7cf6cdd [tint] Add LanguageFeature enum by James Price <jrprice@google.com>
  - 531663f4669bbb466f020b4dd490f6e1dfd3c6b7 Fixup binding generator for duplicate bindings. by dan sinclair <dsinclair@chromium.org>
GitOrigin-RevId: 558f0dfed76840e3793d2468b3b504d465b8ee89
Change-Id: Icfed6d99331e4941506913d87b1f492c07c86120
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/159140
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/api/options/BUILD.bazel b/src/tint/api/options/BUILD.bazel
index 5adc20f..3e1de60 100644
--- a/src/tint/api/options/BUILD.bazel
+++ b/src/tint/api/options/BUILD.bazel
@@ -50,7 +50,6 @@
   ],
   deps = [
     "//src/tint/api/common",
-    "//src/tint/lang/core",
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
     "//src/tint/utils/reflection",
diff --git a/src/tint/api/options/BUILD.cmake b/src/tint/api/options/BUILD.cmake
index d6fb7a5..9cf5e92 100644
--- a/src/tint/api/options/BUILD.cmake
+++ b/src/tint/api/options/BUILD.cmake
@@ -49,7 +49,6 @@
 
 tint_target_add_dependencies(tint_api_options lib
   tint_api_common
-  tint_lang_core
   tint_utils_macros
   tint_utils_math
   tint_utils_reflection
diff --git a/src/tint/api/options/BUILD.gn b/src/tint/api/options/BUILD.gn
index 3c016b5..3b567ca 100644
--- a/src/tint/api/options/BUILD.gn
+++ b/src/tint/api/options/BUILD.gn
@@ -49,7 +49,6 @@
   ]
   deps = [
     "${tint_src_dir}/api/common",
-    "${tint_src_dir}/lang/core",
     "${tint_src_dir}/utils/macros",
     "${tint_src_dir}/utils/math",
     "${tint_src_dir}/utils/reflection",
diff --git a/src/tint/api/options/binding_remapper.h b/src/tint/api/options/binding_remapper.h
index d560493..493c854 100644
--- a/src/tint/api/options/binding_remapper.h
+++ b/src/tint/api/options/binding_remapper.h
@@ -31,7 +31,6 @@
 #include <unordered_map>
 
 #include "src/tint/api/common/binding_point.h"
-#include "src/tint/lang/core/access.h"
 
 namespace tint {
 
diff --git a/src/tint/cmd/fuzz/wgsl/BUILD.bazel b/src/tint/cmd/fuzz/wgsl/BUILD.bazel
index 7c71462..4c77253 100644
--- a/src/tint/cmd/fuzz/wgsl/BUILD.bazel
+++ b/src/tint/cmd/fuzz/wgsl/BUILD.bazel
@@ -42,3 +42,8 @@
   actual = "//src/tint:tint_build_wgsl_reader_true",
 )
 
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
diff --git a/src/tint/cmd/fuzz/wgsl/BUILD.cmake b/src/tint/cmd/fuzz/wgsl/BUILD.cmake
index 9ba6530..9648f95 100644
--- a/src/tint/cmd/fuzz/wgsl/BUILD.cmake
+++ b/src/tint/cmd/fuzz/wgsl/BUILD.cmake
@@ -51,6 +51,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
+  tint_lang_wgsl_program_fuzz
   tint_lang_wgsl_sem
   tint_lang_wgsl_fuzz
   tint_utils_cli
@@ -76,6 +77,12 @@
   )
 endif(TINT_BUILD_WGSL_READER)
 
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_fuzz_wgsl_fuzz_cmd fuzz_cmd
+    tint_lang_wgsl_writer_ast_printer_fuzz
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
 tint_target_set_output_name(tint_cmd_fuzz_wgsl_fuzz_cmd fuzz_cmd "tint_wgsl_fuzzer")
 
 endif(TINT_BUILD_WGSL_READER)
diff --git a/src/tint/cmd/fuzz/wgsl/BUILD.gn b/src/tint/cmd/fuzz/wgsl/BUILD.gn
index 5c4ccc6..5b86127 100644
--- a/src/tint/cmd/fuzz/wgsl/BUILD.gn
+++ b/src/tint/cmd/fuzz/wgsl/BUILD.gn
@@ -86,6 +86,7 @@
       "${tint_src_dir}/lang/wgsl:fuzz",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/program",
+      "${tint_src_dir}/lang/wgsl/program:fuzz",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/cli",
       "${tint_src_dir}/utils/containers",
@@ -109,5 +110,9 @@
         "${tint_src_dir}/lang/wgsl/ast/transform:fuzz",
       ]
     }
+
+    if (tint_build_wgsl_writer) {
+      deps += [ "${tint_src_dir}/lang/wgsl/writer/ast_printer:fuzz" ]
+    }
   }
 }
diff --git a/src/tint/fuzzers/BUILD.gn b/src/tint/fuzzers/BUILD.gn
index 7f5b0cd..375d00d 100644
--- a/src/tint/fuzzers/BUILD.gn
+++ b/src/tint/fuzzers/BUILD.gn
@@ -134,20 +134,6 @@
   }
 
   if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
-    fuzzer_test("tint_ast_clone_fuzzer") {
-      sources = [ "tint_ast_clone_fuzzer.cc" ]
-      deps = [ ":tint_fuzzer_common_with_init_src" ]
-      deps += [
-        "${tint_src_dir}/lang/wgsl/reader",
-        "${tint_src_dir}/lang/wgsl/reader/parser",
-      ]
-
-      dict = "dictionary.txt"
-      libfuzzer_options = tint_fuzzer_common_libfuzzer_options
-      seed_corpus = fuzzer_corpus_wgsl_dir
-      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
-    }
-
     if (build_with_chromium) {
       fuzzer_test("tint_ast_wgsl_writer_fuzzer") {
         sources = [ "tint_ast_fuzzer/tint_ast_wgsl_writer_fuzzer.cc" ]
@@ -165,15 +151,6 @@
       seed_corpus = fuzzer_corpus_wgsl_dir
       seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
     }
-
-    fuzzer_test("tint_wgsl_reader_wgsl_writer_fuzzer") {
-      sources = [ "tint_wgsl_reader_wgsl_writer_fuzzer.cc" ]
-      deps = [ ":tint_fuzzer_common_with_init_src" ]
-      dict = "dictionary.txt"
-      libfuzzer_options = tint_fuzzer_common_libfuzzer_options
-      seed_corpus = fuzzer_corpus_wgsl_dir
-      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
-    }
   }
 
   if (tint_build_wgsl_reader && tint_build_spv_writer) {
@@ -343,11 +320,7 @@
     deps = []
 
     if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
-      deps += [
-        ":tint_ast_clone_fuzzer",
-        ":tint_regex_wgsl_writer_fuzzer",
-        ":tint_wgsl_reader_wgsl_writer_fuzzer",
-      ]
+      deps += [ ":tint_regex_wgsl_writer_fuzzer" ]
       if (build_with_chromium) {
         deps += [ ":tint_ast_wgsl_writer_fuzzer" ]
       }
diff --git a/src/tint/fuzzers/CMakeLists.txt b/src/tint/fuzzers/CMakeLists.txt
index cd5a057..3801e7f 100644
--- a/src/tint/fuzzers/CMakeLists.txt
+++ b/src/tint/fuzzers/CMakeLists.txt
@@ -55,10 +55,6 @@
   target_compile_options(${NAME} PRIVATE -Wno-missing-prototypes)
 endfunction()
 
-if (${TINT_BUILD_WGSL_READER} AND ${TINT_BUILD_WGSL_WRITER})
-  add_tint_fuzzer(tint_wgsl_reader_wgsl_writer_fuzzer)
-endif()
-
 if (${TINT_BUILD_WGSL_READER} AND ${TINT_BUILD_SPV_WRITER})
   add_tint_fuzzer(tint_all_transforms_fuzzer)
   add_tint_fuzzer(tint_binding_remapper_fuzzer)
@@ -95,10 +91,6 @@
   add_tint_fuzzer(tint_spv_reader_msl_writer_fuzzer)
 endif()
 
-if (${TINT_BUILD_WGSL_READER} AND ${TINT_BUILD_WGSL_WRITER})
-  add_tint_fuzzer(tint_ast_clone_fuzzer)
-endif()
-
 if (${TINT_BUILD_AST_FUZZER})
   add_subdirectory(tint_ast_fuzzer)
 endif()
diff --git a/src/tint/fuzzers/tint_ast_clone_fuzzer.cc b/src/tint/fuzzers/tint_ast_clone_fuzzer.cc
deleted file mode 100644
index e33d3f9..0000000
--- a/src/tint/fuzzers/tint_ast_clone_fuzzer.cc
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright 2020 The Dawn & Tint Authors
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-// 1. Redistributions of source code must retain the above copyright notice, this
-//    list of conditions and the following disclaimer.
-//
-// 2. Redistributions in binary form must reproduce the above copyright notice,
-//    this list of conditions and the following disclaimer in the documentation
-//    and/or other materials provided with the distribution.
-//
-// 3. Neither the name of the copyright holder nor the names of its
-//    contributors may be used to endorse or promote products derived from
-//    this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-#include <iostream>
-#include <string>
-#include <unordered_set>
-
-#include "src/tint/lang/wgsl/reader/parser/parser.h"
-#include "src/tint/lang/wgsl/writer/writer.h"
-
-#define ASSERT_EQ(A, B)                                        \
-    do {                                                       \
-        decltype(A) assert_a = (A);                            \
-        decltype(B) assert_b = (B);                            \
-        if (assert_a != assert_b) {                            \
-            std::cerr << "ASSERT_EQ(" #A ", " #B ") failed:\n" \
-                      << #A << " was: " << assert_a << "\n"    \
-                      << #B << " was: " << assert_b << "\n";   \
-            __builtin_trap();                                  \
-        }                                                      \
-    } while (false)
-
-#define ASSERT_TRUE(A)                                                                          \
-    do {                                                                                        \
-        decltype(A) assert_a = (A);                                                             \
-        if (!assert_a) {                                                                        \
-            std::cerr << "ASSERT_TRUE(" #A ") failed:\n" << #A << " was: " << assert_a << "\n"; \
-            __builtin_trap();                                                                   \
-        }                                                                                       \
-    } while (false)
-
-[[noreturn]] void TintInternalCompilerErrorReporter(const tint::InternalCompilerError& err) {
-    std::cerr << err.Error() << std::endl;
-    __builtin_trap();
-}
-
-extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
-    std::string str(reinterpret_cast<const char*>(data), size);
-
-    tint::SetInternalCompilerErrorReporter(&TintInternalCompilerErrorReporter);
-
-    tint::Source::File file("test.wgsl", str);
-
-    // Parse the wgsl, create the src program
-    tint::wgsl::reader::Parser parser(&file);
-    parser.set_max_errors(1);
-    if (!parser.Parse()) {
-        return 0;
-    }
-    auto src = parser.program();
-    if (!src.IsValid()) {
-        return 0;
-    }
-
-    // Clone the src program to dst
-    tint::Program dst(src.Clone());
-
-    // Expect the printed strings to match
-    ASSERT_EQ(tint::Program::printer(src), tint::Program::printer(dst));
-
-    // Check that none of the AST nodes or type pointers in dst are found in src
-    std::unordered_set<const tint::ast::Node*> src_nodes;
-    for (auto* src_node : src.ASTNodes().Objects()) {
-        src_nodes.emplace(src_node);
-    }
-    std::unordered_set<const tint::core::type::Type*> src_types;
-    for (auto* src_type : src.Types()) {
-        src_types.emplace(src_type);
-    }
-    for (auto* dst_node : dst.ASTNodes().Objects()) {
-        ASSERT_EQ(src_nodes.count(dst_node), 0u);
-    }
-    for (auto* dst_type : dst.Types()) {
-        ASSERT_EQ(src_types.count(dst_type), 0u);
-    }
-
-    // Regenerate the wgsl for the src program. We use this instead of the
-    // original source so that reformatting doesn't impact the final wgsl
-    // comparison.
-    std::string src_wgsl;
-    tint::wgsl::writer::Options wgsl_options;
-    {
-        auto result = tint::wgsl::writer::Generate(src, wgsl_options);
-        ASSERT_TRUE(result == true);
-        src_wgsl = result->wgsl;
-
-        // Move the src program to a temporary that'll be dropped, so that the src
-        // program is released before we attempt to print the dst program. This
-        // guarantee that all the source program nodes and types are destructed and
-        // freed. ASAN should error if there's any remaining references in dst when
-        // we try to reconstruct the WGSL.
-        auto tmp = std::move(src);
-    }
-
-    // Print the dst program, check it matches the original source
-    auto result = tint::wgsl::writer::Generate(dst, wgsl_options);
-    ASSERT_TRUE(result == true);
-    auto dst_wgsl = result->wgsl;
-    ASSERT_EQ(src_wgsl, dst_wgsl);
-
-    return 0;
-}
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
index cec5d7e..b0c7219 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
@@ -319,6 +319,9 @@
                 // Record the required extension for generating extension directive later
                 RecordExtension(enable);
             },  //
+            [&](const ast::Requires*) {
+                // Do nothing for requiring language features in GLSL.
+            },  //
             TINT_ICE_ON_NO_MATCH);
     }
 
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc
index 7d70089..9834a54 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc
@@ -131,5 +131,16 @@
               R"(12:34 error: GLSL backend does not support extension 'undefined')");
 }
 
+TEST_F(GlslASTPrinterTest, RequiresDirective) {
+    Require(wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures);
+
+    ASTPrinter& gen = Build();
+
+    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    EXPECT_EQ(gen.Result(), R"(#version 310 es
+
+)");
+}
+
 }  // namespace
 }  // namespace tint::glsl::writer
diff --git a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
index d84e98a..b23c19f 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
@@ -370,7 +370,8 @@
 
     auto* mod = builder_.Sem().Module();
     for (auto* decl : mod->DependencyOrderedDeclarations()) {
-        if (decl->IsAnyOf<ast::Alias, ast::DiagnosticDirective, ast::Enable, ast::ConstAssert>()) {
+        if (decl->IsAnyOf<ast::Alias, ast::DiagnosticDirective, ast::Enable, ast::Requires,
+                          ast::ConstAssert>()) {
             continue;  // These are not emitted.
         }
 
diff --git a/src/tint/lang/hlsl/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/hlsl/writer/ast_printer/ast_printer_test.cc
index a42a651..23c9713 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer_test.cc
@@ -54,6 +54,15 @@
               R"(12:34 error: HLSL backend does not support extension 'undefined')");
 }
 
+TEST_F(HlslASTPrinterTest, RequiresDirective) {
+    Require(wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures);
+
+    ASTPrinter& gen = Build();
+
+    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    EXPECT_EQ(gen.Result(), "");
+}
+
 TEST_F(HlslASTPrinterTest, Generate) {
     Func("my_func", {}, ty.void_(), {});
 
diff --git a/src/tint/lang/hlsl/writer/common/options.h b/src/tint/lang/hlsl/writer/common/options.h
index b71fe89..e8d5cc0 100644
--- a/src/tint/lang/hlsl/writer/common/options.h
+++ b/src/tint/lang/hlsl/writer/common/options.h
@@ -37,6 +37,7 @@
 #include "src/tint/api/options/array_length_from_uniform.h"
 #include "src/tint/api/options/binding_remapper.h"
 #include "src/tint/api/options/external_texture.h"
+#include "src/tint/lang/core/access.h"
 #include "src/tint/utils/reflection/reflection.h"
 
 namespace tint::hlsl::writer {
diff --git a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
index 69a4f31..3d49d28 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
@@ -325,6 +325,10 @@
                 // Do nothing for enabling extension in MSL
                 return true;
             },
+            [&](const ast::Requires*) {
+                // Do nothing for requiring language features in MSL.
+                return true;
+            },
             [&](const ast::ConstAssert*) {
                 return true;  // Not emitted
             },                //
diff --git a/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc
index 3b738f5..49fa3c1 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc
@@ -58,6 +58,18 @@
               R"(12:34 error: MSL backend does not support extension 'undefined')");
 }
 
+TEST_F(MslASTPrinterTest, RequiresDirective) {
+    Require(wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures);
+
+    ASTPrinter& gen = Build();
+
+    ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
+    EXPECT_EQ(gen.Result(), R"(#include <metal_stdlib>
+
+using namespace metal;
+)");
+}
+
 TEST_F(MslASTPrinterTest, Generate) {
     Func("my_func", tint::Empty, ty.void_(), tint::Empty,
          Vector{
diff --git a/src/tint/lang/spirv/reader/ast_parser/ast_parser.cc b/src/tint/lang/spirv/reader/ast_parser/ast_parser.cc
index 0bf7f61..9370817 100644
--- a/src/tint/lang/spirv/reader/ast_parser/ast_parser.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/ast_parser.cc
@@ -2583,6 +2583,9 @@
         } else {
             const auto access =
                 usage.IsStorageReadWriteTexture() ? core::Access::kReadWrite : core::Access::kWrite;
+            if (access == core::Access::kReadWrite) {
+                Require(wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures);
+            }
             const auto format = enum_converter_.ToTexelFormat(image_type->format());
             if (format == core::TexelFormat::kUndefined) {
                 return nullptr;
diff --git a/src/tint/lang/spirv/reader/ast_parser/ast_parser.h b/src/tint/lang/spirv/reader/ast_parser/ast_parser.h
index 59d3c02..ddd8ded 100644
--- a/src/tint/lang/spirv/reader/ast_parser/ast_parser.h
+++ b/src/tint/lang/spirv/reader/ast_parser/ast_parser.h
@@ -759,6 +759,14 @@
         }
     }
 
+    /// Require a WGSL language feature, if not already required.
+    /// @param feature the language feature to require
+    void Require(wgsl::LanguageFeature feature) {
+        if (required_features_.Add(feature)) {
+            builder_.Require(feature);
+        }
+    }
+
   private:
     /// Converts a specific SPIR-V type to a Tint type. Integer case
     const Type* ConvertType(const spvtools::opt::analysis::Integer* int_ty);
@@ -948,6 +956,8 @@
 
     /// Set of WGSL extensions that have been enabled.
     Hashset<wgsl::Extension, 4> enabled_extensions_;
+    /// Set of WGSL language features that have been required.
+    Hashset<wgsl::LanguageFeature, 4> required_features_;
 };
 
 }  // namespace tint::spirv::reader::ast_parser
diff --git a/src/tint/lang/spirv/reader/ast_parser/function.cc b/src/tint/lang/spirv/reader/ast_parser/function.cc
index 198f58f..2ba5673 100644
--- a/src/tint/lang/spirv/reader/ast_parser/function.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/function.cc
@@ -5325,6 +5325,7 @@
         if (memory != uint32_t(spv::Scope::Workgroup)) {
             return Fail() << "textureBarrier requires workgroup memory scope";
         }
+        parser_impl_.Require(wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures);
         AddStatement(builder_.CallStmt(builder_.Call("textureBarrier")));
         semantics &= ~static_cast<uint32_t>(spv::MemorySemanticsMask::ImageMemory);
     }
diff --git a/src/tint/lang/spirv/reader/ast_parser/handle_test.cc b/src/tint/lang/spirv/reader/ast_parser/handle_test.cc
index 154ccc2..dd05ded 100644
--- a/src/tint/lang/spirv/reader/ast_parser/handle_test.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/handle_test.cc
@@ -4234,7 +4234,9 @@
     EXPECT_TRUE(p->error().empty()) << p->error();
     const auto got = test::ToString(p->program());
     auto* expect =
-        R"(@group(0) @binding(0) var RWTexture2D : texture_storage_2d<rgba32float, read_write>;
+        R"(requires readonly_and_readwrite_storage_textures;
+
+@group(0) @binding(0) var RWTexture2D : texture_storage_2d<rgba32float, read_write>;
 
 const x_9 = vec2u(1u);
 
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/spirv/writer/ast_printer/ast_printer_test.cc
index 932ab4d..9fda078 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_printer_test.cc
@@ -53,5 +53,13 @@
               R"(12:34 error: SPIR-V backend does not support extension 'undefined')");
 }
 
+TEST_F(SpirvASTPrinterTest, RequiresDirective) {
+    Require(wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures);
+
+    auto program = resolver::Resolve(*this);
+    auto result = Generate(program, Options{});
+    EXPECT_TRUE(result);
+}
+
 }  // namespace
 }  // namespace tint::spirv::writer
diff --git a/src/tint/lang/spirv/writer/common/option_builder.cc b/src/tint/lang/spirv/writer/common/option_builder.cc
index 6b66447..55181fc 100644
--- a/src/tint/lang/spirv/writer/common/option_builder.cc
+++ b/src/tint/lang/spirv/writer/common/option_builder.cc
@@ -32,31 +32,41 @@
 namespace tint::spirv::writer {
 
 bool ValidateBindingOptions(const Options& options, diag::List& diagnostics) {
-    tint::Hashset<tint::BindingPoint, 8> seen_wgsl_bindings{};
-    tint::Hashset<binding::BindingInfo, 8> seen_spirv_bindings{};
+    tint::Hashmap<tint::BindingPoint, binding::BindingInfo, 8> seen_wgsl_bindings{};
+    tint::Hashmap<binding::BindingInfo, tint::BindingPoint, 8> seen_spirv_bindings{};
 
-    auto wgsl_seen = [&diagnostics, &seen_wgsl_bindings](const tint::BindingPoint& info) -> bool {
-        if (seen_wgsl_bindings.Contains(info)) {
-            std::stringstream str;
-            str << "found duplicate WGSL binding point: " << info;
+    // Both wgsl_seen and spirv_seen check to see if the pair of [src, dst] are unique. If we have
+    // multiple entries that map the same [src, dst] pair, that's fine. We treat it as valid as it's
+    // possible for multiple entry points to use the remapper at the same time. If the pair doesn't
+    // match, then we report an error about a duplicate binding point.
 
-            diagnostics.add_error(diag::System::Writer, str.str());
-            return true;
+    auto wgsl_seen = [&diagnostics, &seen_wgsl_bindings](const tint::BindingPoint& src,
+                                                         const binding::BindingInfo& dst) -> bool {
+        if (auto binding = seen_wgsl_bindings.Find(src)) {
+            if (*binding != dst) {
+                std::stringstream str;
+                str << "found duplicate WGSL binding point: " << src;
+
+                diagnostics.add_error(diag::System::Writer, str.str());
+                return true;
+            }
         }
-        seen_wgsl_bindings.Add(info);
+        seen_wgsl_bindings.Add(src, dst);
         return false;
     };
 
-    auto spirv_seen = [&diagnostics,
-                       &seen_spirv_bindings](const binding::BindingInfo& info) -> bool {
-        if (seen_spirv_bindings.Contains(info)) {
-            std::stringstream str;
-            str << "found duplicate SPIR-V binding point: [group: " << info.group
-                << ", binding: " << info.binding << "]";
-            diagnostics.add_error(diag::System::Writer, str.str());
-            return true;
+    auto spirv_seen = [&diagnostics, &seen_spirv_bindings](const binding::BindingInfo& src,
+                                                           const tint::BindingPoint& dst) -> bool {
+        if (auto binding = seen_spirv_bindings.Find(src)) {
+            if (*binding != dst) {
+                std::stringstream str;
+                str << "found duplicate SPIR-V binding point: [group: " << src.group
+                    << ", binding: " << src.binding << "]";
+                diagnostics.add_error(diag::System::Writer, str.str());
+                return true;
+            }
         }
-        seen_spirv_bindings.Add(info);
+        seen_spirv_bindings.Add(src, dst);
         return false;
     };
 
@@ -65,11 +75,11 @@
             const auto& src_binding = it.first;
             const auto& dst_binding = it.second;
 
-            if (wgsl_seen(src_binding)) {
+            if (wgsl_seen(src_binding, dst_binding)) {
                 return false;
             }
 
-            if (spirv_seen(dst_binding)) {
+            if (spirv_seen(dst_binding, src_binding)) {
                 return false;
             }
         }
@@ -104,20 +114,20 @@
         const auto& metadata = it.second.metadata;
 
         // Validate with the actual source regardless of what the remapper will do
-        if (wgsl_seen(src_binding)) {
+        if (wgsl_seen(src_binding, plane0)) {
             diagnostics.add_note(diag::System::Writer, "when processing external_texture", {});
             return false;
         }
 
-        if (spirv_seen(plane0)) {
+        if (spirv_seen(plane0, src_binding)) {
             diagnostics.add_note(diag::System::Writer, "when processing external_texture", {});
             return false;
         }
-        if (spirv_seen(plane1)) {
+        if (spirv_seen(plane1, src_binding)) {
             diagnostics.add_note(diag::System::Writer, "when processing external_texture", {});
             return false;
         }
-        if (spirv_seen(metadata)) {
+        if (spirv_seen(metadata, src_binding)) {
             diagnostics.add_note(diag::System::Writer, "when processing external_texture", {});
             return false;
         }
diff --git a/src/tint/lang/spirv/writer/common/options.h b/src/tint/lang/spirv/writer/common/options.h
index c0f5e28..96b585e 100644
--- a/src/tint/lang/spirv/writer/common/options.h
+++ b/src/tint/lang/spirv/writer/common/options.h
@@ -49,6 +49,10 @@
     inline bool operator==(const BindingInfo& rhs) const {
         return group == rhs.group && binding == rhs.binding;
     }
+    /// Inequality operator
+    /// @param rhs the BindingInfo to compare against
+    /// @returns true if this BindingInfo is not equal to `rhs`
+    inline bool operator!=(const BindingInfo& rhs) const { return !(*this == rhs); }
 
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
     TINT_REFLECT(group, binding);
diff --git a/src/tint/lang/spirv/writer/helpers/generate_bindings.cc b/src/tint/lang/spirv/writer/helpers/generate_bindings.cc
index 1f429ba..21ef88b 100644
--- a/src/tint/lang/spirv/writer/helpers/generate_bindings.cc
+++ b/src/tint/lang/spirv/writer/helpers/generate_bindings.cc
@@ -50,28 +50,12 @@
 
     Bindings bindings{};
 
-    std::unordered_set<tint::BindingPoint> seen_binding_points;
-
     // Collect next valid binding number per group
     Hashmap<uint32_t, uint32_t, 4> group_to_next_binding_number;
     Vector<tint::BindingPoint, 4> ext_tex_bps;
     for (auto* var : program.AST().GlobalVariables()) {
         if (auto* sem_var = program.Sem().Get(var)->As<sem::GlobalVariable>()) {
             if (auto bp = sem_var->BindingPoint()) {
-                // This is a bit of a hack. The binding points must be unique over all the `binding`
-                // entries. But, this is looking at _all_ entry points where bindings can overlap.
-                // In the case where both entry points used the same type (uniform, sampler, etc)
-                // then it would be fine as it just overwrites with itself. But, if one entry point
-                // has a uniform and the other a sampler at the same (group,binding) pair then we'll
-                // get a validation error due to duplicate WGSL bindings.
-                //
-                // For generating bindings we don't really care as we always map to itself, so if it
-                // exists anywhere, we just pretend that's the only one.
-                if (seen_binding_points.find(*bp) != seen_binding_points.end()) {
-                    continue;
-                }
-                seen_binding_points.emplace(*bp);
-
                 if (auto val = group_to_next_binding_number.Find(bp->group)) {
                     *val = std::max(*val, bp->binding + 1);
                 } else {
diff --git a/src/tint/lang/wgsl/BUILD.bazel b/src/tint/lang/wgsl/BUILD.bazel
index 9b7655d..0dd8726 100644
--- a/src/tint/lang/wgsl/BUILD.bazel
+++ b/src/tint/lang/wgsl/BUILD.bazel
@@ -43,12 +43,14 @@
     "diagnostic_rule.cc",
     "diagnostic_severity.cc",
     "extension.cc",
+    "language_feature.cc",
   ],
   hdrs = [
     "builtin_fn.h",
     "diagnostic_rule.h",
     "diagnostic_severity.h",
     "extension.h",
+    "language_feature.h",
   ],
   deps = [
     "//src/tint/utils/containers",
@@ -71,6 +73,7 @@
     "diagnostic_rule_test.cc",
     "diagnostic_severity_test.cc",
     "extension_test.cc",
+    "language_feature_test.cc",
     "wgsl_test.cc",
   ] + select({
     "//conditions:default": [],
diff --git a/src/tint/lang/wgsl/BUILD.cmake b/src/tint/lang/wgsl/BUILD.cmake
index cea7aa0..7a1599d 100644
--- a/src/tint/lang/wgsl/BUILD.cmake
+++ b/src/tint/lang/wgsl/BUILD.cmake
@@ -58,6 +58,8 @@
   lang/wgsl/diagnostic_severity.h
   lang/wgsl/extension.cc
   lang/wgsl/extension.h
+  lang/wgsl/language_feature.cc
+  lang/wgsl/language_feature.h
 )
 
 tint_target_add_dependencies(tint_lang_wgsl lib
@@ -80,6 +82,7 @@
   lang/wgsl/diagnostic_rule_test.cc
   lang/wgsl/diagnostic_severity_test.cc
   lang/wgsl/extension_test.cc
+  lang/wgsl/language_feature_test.cc
   lang/wgsl/wgsl_test.cc
 )
 
diff --git a/src/tint/lang/wgsl/BUILD.gn b/src/tint/lang/wgsl/BUILD.gn
index 10d334a..619e1f8 100644
--- a/src/tint/lang/wgsl/BUILD.gn
+++ b/src/tint/lang/wgsl/BUILD.gn
@@ -52,6 +52,8 @@
     "diagnostic_severity.h",
     "extension.cc",
     "extension.h",
+    "language_feature.cc",
+    "language_feature.h",
   ]
   deps = [
     "${tint_src_dir}/utils/containers",
@@ -71,6 +73,7 @@
       "diagnostic_rule_test.cc",
       "diagnostic_severity_test.cc",
       "extension_test.cc",
+      "language_feature_test.cc",
       "wgsl_test.cc",
     ]
     deps = [
diff --git a/src/tint/lang/wgsl/ast/BUILD.bazel b/src/tint/lang/wgsl/ast/BUILD.bazel
index eabb9ca..bc6462d 100644
--- a/src/tint/lang/wgsl/ast/BUILD.bazel
+++ b/src/tint/lang/wgsl/ast/BUILD.bazel
@@ -97,6 +97,7 @@
     "parameter.cc",
     "phony_expression.cc",
     "pipeline_stage.cc",
+    "requires.cc",
     "return_statement.cc",
     "stage_attribute.cc",
     "statement.cc",
@@ -177,6 +178,7 @@
     "parameter.h",
     "phony_expression.h",
     "pipeline_stage.h",
+    "requires.h",
     "return_statement.h",
     "stage_attribute.h",
     "statement.h",
@@ -271,6 +273,7 @@
     "member_accessor_expression_test.cc",
     "module_test.cc",
     "phony_expression_test.cc",
+    "requires_test.cc",
     "return_statement_test.cc",
     "stage_attribute_test.cc",
     "stride_attribute_test.cc",
diff --git a/src/tint/lang/wgsl/ast/BUILD.cmake b/src/tint/lang/wgsl/ast/BUILD.cmake
index e72ea46..e35a8b1 100644
--- a/src/tint/lang/wgsl/ast/BUILD.cmake
+++ b/src/tint/lang/wgsl/ast/BUILD.cmake
@@ -158,6 +158,8 @@
   lang/wgsl/ast/phony_expression.h
   lang/wgsl/ast/pipeline_stage.cc
   lang/wgsl/ast/pipeline_stage.h
+  lang/wgsl/ast/requires.cc
+  lang/wgsl/ast/requires.h
   lang/wgsl/ast/return_statement.cc
   lang/wgsl/ast/return_statement.h
   lang/wgsl/ast/stage_attribute.cc
@@ -271,6 +273,7 @@
   lang/wgsl/ast/member_accessor_expression_test.cc
   lang/wgsl/ast/module_test.cc
   lang/wgsl/ast/phony_expression_test.cc
+  lang/wgsl/ast/requires_test.cc
   lang/wgsl/ast/return_statement_test.cc
   lang/wgsl/ast/stage_attribute_test.cc
   lang/wgsl/ast/stride_attribute_test.cc
diff --git a/src/tint/lang/wgsl/ast/BUILD.gn b/src/tint/lang/wgsl/ast/BUILD.gn
index e440921..9c36fd0 100644
--- a/src/tint/lang/wgsl/ast/BUILD.gn
+++ b/src/tint/lang/wgsl/ast/BUILD.gn
@@ -161,6 +161,8 @@
     "phony_expression.h",
     "pipeline_stage.cc",
     "pipeline_stage.h",
+    "requires.cc",
+    "requires.h",
     "return_statement.cc",
     "return_statement.h",
     "stage_attribute.cc",
@@ -271,6 +273,7 @@
       "member_accessor_expression_test.cc",
       "module_test.cc",
       "phony_expression_test.cc",
+      "requires_test.cc",
       "return_statement_test.cc",
       "stage_attribute_test.cc",
       "stride_attribute_test.cc",
diff --git a/src/tint/lang/wgsl/ast/builder.h b/src/tint/lang/wgsl/ast/builder.h
index bab8fb9..1cbdfba 100644
--- a/src/tint/lang/wgsl/ast/builder.h
+++ b/src/tint/lang/wgsl/ast/builder.h
@@ -97,6 +97,7 @@
 #include "src/tint/lang/wgsl/ast/override.h"
 #include "src/tint/lang/wgsl/ast/parameter.h"
 #include "src/tint/lang/wgsl/ast/phony_expression.h"
+#include "src/tint/lang/wgsl/ast/requires.h"
 #include "src/tint/lang/wgsl/ast/return_statement.h"
 #include "src/tint/lang/wgsl/ast/stage_attribute.h"
 #include "src/tint/lang/wgsl/ast/stride_attribute.h"
@@ -1617,6 +1618,25 @@
         return enable;
     }
 
+    /// Adds the language feature to the list of requires directives at the top of the module.
+    /// @param feature the feature to require
+    /// @return a `ast::Requires` requiring the given language feature.
+    const ast::Requires* Require(wgsl::LanguageFeature feature) {
+        auto* req = create<ast::Requires>(wgsl::LanguageFeatures({feature}));
+        AST().AddRequires(req);
+        return req;
+    }
+
+    /// Adds the language feature to the list of requires directives at the top of the module.
+    /// @param source the requires source
+    /// @param feature the feature to require
+    /// @return a `ast::Requires` requiring the given language feature.
+    const ast::Requires* Require(const Source& source, wgsl::LanguageFeature feature) {
+        auto* req = create<ast::Requires>(source, wgsl::LanguageFeatures({feature}));
+        AST().AddRequires(req);
+        return req;
+    }
+
     /// @param name the variable name
     /// @param options the extra options passed to the ast::Var initializer
     /// Can be any of the following, in any order:
diff --git a/src/tint/lang/wgsl/ast/module.cc b/src/tint/lang/wgsl/ast/module.cc
index 3ee8cfd..974abad 100644
--- a/src/tint/lang/wgsl/ast/module.cc
+++ b/src/tint/lang/wgsl/ast/module.cc
@@ -89,6 +89,10 @@
             TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(enable, generation_id);
             enables_.Push(enable);
         },
+        [&](const ast::Requires* req) {
+            TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(req, generation_id);
+            requires_.Push(req);
+        },
         [&](const ConstAssert* assertion) {
             TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(assertion, generation_id);
             const_asserts_.Push(assertion);
@@ -110,6 +114,13 @@
     enables_.Push(enable);
 }
 
+void Module::AddRequires(const ast::Requires* req) {
+    TINT_ASSERT(req);
+    TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(req, generation_id);
+    global_declarations_.Push(req);
+    requires_.Push(req);
+}
+
 void Module::AddGlobalVariable(const Variable* var) {
     TINT_ASSERT(var);
     TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(var, generation_id);
diff --git a/src/tint/lang/wgsl/ast/module.h b/src/tint/lang/wgsl/ast/module.h
index 7214aec..8513348 100644
--- a/src/tint/lang/wgsl/ast/module.h
+++ b/src/tint/lang/wgsl/ast/module.h
@@ -34,6 +34,7 @@
 #include "src/tint/lang/wgsl/ast/diagnostic_directive.h"
 #include "src/tint/lang/wgsl/ast/enable.h"
 #include "src/tint/lang/wgsl/ast/function.h"
+#include "src/tint/lang/wgsl/ast/requires.h"
 #include "src/tint/utils/containers/vector.h"
 
 namespace tint::ast {
@@ -110,12 +111,19 @@
     /// @param ext the enable directive to add
     void AddEnable(const Enable* ext);
 
+    /// Add a requires directive to the module
+    /// @param req the requires directive to add
+    void AddRequires(const Requires* req);
+
     /// @returns the diagnostic directives for the module
     const auto& DiagnosticDirectives() const { return diagnostic_directives_; }
 
     /// @returns the extension set for the module
     const auto& Enables() const { return enables_; }
 
+    /// @returns the requires directives for the module
+    const auto& Requires() const { return requires_; }
+
     /// Add a global const assertion to the module
     /// @param assertion the const assert to add
     void AddConstAssert(const ConstAssert* assertion);
@@ -168,6 +176,7 @@
     tint::Vector<const Variable*, 32> global_variables_;
     tint::Vector<const DiagnosticDirective*, 8> diagnostic_directives_;
     tint::Vector<const Enable*, 8> enables_;
+    tint::Vector<const ast::Requires*, 8> requires_;
     tint::Vector<const ConstAssert*, 8> const_asserts_;
 };
 
diff --git a/src/tint/lang/wgsl/ast/module_clone_test.cc b/src/tint/lang/wgsl/ast/module_clone_test.cc
index 6e64f65..3444805 100644
--- a/src/tint/lang/wgsl/ast/module_clone_test.cc
+++ b/src/tint/lang/wgsl/ast/module_clone_test.cc
@@ -38,7 +38,6 @@
 
 TEST(ModuleCloneTest, Clone) {
     // 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"(enable f16;
 diagnostic(off, chromium.unreachable_code);
 
diff --git a/src/tint/fuzzers/tint_wgsl_reader_wgsl_writer_fuzzer.cc b/src/tint/lang/wgsl/ast/requires.cc
similarity index 70%
copy from src/tint/fuzzers/tint_wgsl_reader_wgsl_writer_fuzzer.cc
copy to src/tint/lang/wgsl/ast/requires.cc
index 7ea3510..059c35f 100644
--- a/src/tint/fuzzers/tint_wgsl_reader_wgsl_writer_fuzzer.cc
+++ b/src/tint/lang/wgsl/ast/requires.cc
@@ -1,4 +1,4 @@
-// Copyright 2021 The Dawn & Tint Authors
+// Copyright 2023 The Dawn & Tint Authors
 //
 // Redistribution and use in source and binary forms, with or without
 // modification, are permitted provided that the following conditions are met:
@@ -25,19 +25,23 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#include <string>
+#include "src/tint/lang/wgsl/ast/requires.h"
 
-#include "src/tint/fuzzers/fuzzer_init.h"
-#include "src/tint/fuzzers/tint_reader_writer_fuzzer.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
-namespace tint::fuzzers {
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Requires);
 
-extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
-    tint::fuzzers::ReaderWriterFuzzer fuzzer(InputFormat::kWGSL, OutputFormat::kWGSL);
-    fuzzer.SetDumpInput(GetCliParams().dump_input);
-    fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+namespace tint::ast {
 
-    return fuzzer.Run(data, size);
+Requires::Requires(GenerationID pid, NodeID nid, const Source& src, wgsl::LanguageFeatures feats)
+    : Base(pid, nid, src), features(std::move(feats)) {}
+
+Requires::~Requires() = default;
+
+const Requires* Requires::Clone(CloneContext& ctx) const {
+    auto src = ctx.Clone(source);
+    return ctx.dst->create<Requires>(src, features);
 }
 
-}  // namespace tint::fuzzers
+}  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/requires.h b/src/tint/lang/wgsl/ast/requires.h
new file mode 100644
index 0000000..c94d6b0
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/requires.h
@@ -0,0 +1,68 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_WGSL_AST_REQUIRES_H_
+#define SRC_TINT_LANG_WGSL_AST_REQUIRES_H_
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "src/tint/lang/wgsl/ast/node.h"
+#include "src/tint/lang/wgsl/language_feature.h"
+
+namespace tint::ast {
+
+/// A "requires" directive. Example:
+/// ```
+///   // Require a language feature named "foo"
+///   requires foo;
+/// ```
+class Requires final : public Castable<Requires, Node> {
+  public:
+    /// Create a requires directive
+    /// @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 feats the language features being required by this directive
+    Requires(GenerationID pid, NodeID nid, const Source& src, wgsl::LanguageFeatures feats);
+
+    /// Destructor
+    ~Requires() override;
+
+    /// Clones this node and all transitive child nodes using the `CloneContext` `ctx`.
+    /// @param ctx the clone context
+    /// @return the newly cloned node
+    const Requires* Clone(CloneContext& ctx) const override;
+
+    /// The features being required by this directive.
+    const wgsl::LanguageFeatures features;
+};
+
+}  // namespace tint::ast
+
+#endif  // SRC_TINT_LANG_WGSL_AST_REQUIRES_H_
diff --git a/src/tint/fuzzers/tint_wgsl_reader_wgsl_writer_fuzzer.cc b/src/tint/lang/wgsl/ast/requires_test.cc
similarity index 64%
copy from src/tint/fuzzers/tint_wgsl_reader_wgsl_writer_fuzzer.cc
copy to src/tint/lang/wgsl/ast/requires_test.cc
index 7ea3510..8650f3f 100644
--- a/src/tint/fuzzers/tint_wgsl_reader_wgsl_writer_fuzzer.cc
+++ b/src/tint/lang/wgsl/ast/requires_test.cc
@@ -1,4 +1,4 @@
-// Copyright 2021 The Dawn & Tint Authors
+// Copyright 2023 The Dawn & Tint Authors
 //
 // Redistribution and use in source and binary forms, with or without
 // modification, are permitted provided that the following conditions are met:
@@ -25,19 +25,27 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#include <string>
+#include "src/tint/lang/wgsl/ast/requires.h"
 
-#include "src/tint/fuzzers/fuzzer_init.h"
-#include "src/tint/fuzzers/tint_reader_writer_fuzzer.h"
+#include "src/tint/lang/wgsl/ast/helper_test.h"
 
-namespace tint::fuzzers {
+namespace tint::ast {
+namespace {
 
-extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
-    tint::fuzzers::ReaderWriterFuzzer fuzzer(InputFormat::kWGSL, OutputFormat::kWGSL);
-    fuzzer.SetDumpInput(GetCliParams().dump_input);
-    fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+using RequiresTest = TestHelper;
 
-    return fuzzer.Run(data, size);
+TEST_F(RequiresTest, Creation) {
+    auto* req = Require(Source{{{20, 2}, {20, 5}}},
+                        wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures);
+    EXPECT_EQ(req->source.range.begin.line, 20u);
+    EXPECT_EQ(req->source.range.begin.column, 2u);
+    EXPECT_EQ(req->source.range.end.line, 20u);
+    EXPECT_EQ(req->source.range.end.column, 5u);
+    ASSERT_EQ(req->features.Length(), 1u);
+    EXPECT_EQ(req->features[0], wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures);
+    ASSERT_EQ(AST().Requires().Length(), 1u);
+    EXPECT_EQ(AST().Requires()[0], req);
 }
 
-}  // namespace tint::fuzzers
+}  // namespace
+}  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/transform/single_entry_point.cc b/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
index 44e4b86..daae6f8 100644
--- a/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
+++ b/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
@@ -128,6 +128,10 @@
                 }
             },
             [&](const Enable* ext) { b.AST().AddEnable(ctx.Clone(ext)); },
+            [&](const Requires*) {
+                // Drop requires directives as they are optional, and it's non-trivial to determine
+                // which features are needed for which entry points.
+            },
             [&](const DiagnosticDirective* d) { b.AST().AddDiagnosticDirective(ctx.Clone(d)); },  //
             TINT_ICE_ON_NO_MATCH);
     }
diff --git a/src/tint/lang/wgsl/ast/transform/single_entry_point_test.cc b/src/tint/lang/wgsl/ast/transform/single_entry_point_test.cc
index 609f97b..f22ddb5 100644
--- a/src/tint/lang/wgsl/ast/transform/single_entry_point_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/single_entry_point_test.cc
@@ -644,5 +644,30 @@
     EXPECT_EQ(src, str(got));
 }
 
+TEST_F(SingleEntryPointTest, Requires) {
+    // Make sure that requires are handled (and dropped).
+    auto* src = R"(
+requires readonly_and_readwrite_storage_textures;
+
+@compute @workgroup_size(1)
+fn main() {
+}
+)";
+
+    auto* expect = R"(
+@compute @workgroup_size(1)
+fn main() {
+}
+)";
+
+    SingleEntryPoint::Config cfg("main");
+
+    DataMap data;
+    data.Add<SingleEntryPoint::Config>(cfg);
+    auto got = Run<SingleEntryPoint>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
 }  // namespace
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/language_feature.cc b/src/tint/lang/wgsl/language_feature.cc
new file mode 100644
index 0000000..a5e0430
--- /dev/null
+++ b/src/tint/lang/wgsl/language_feature.cc
@@ -0,0 +1,61 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+////////////////////////////////////////////////////////////////////////////////
+// File generated by 'tools/src/cmd/gen' using the template:
+//   src/tint/lang/wgsl/language_feature.cc.tmpl
+//
+// To regenerate run: './tools/run gen'
+//
+//                       Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
+#include "src/tint/lang/wgsl/language_feature.h"
+
+namespace tint::wgsl {
+
+/// ParseLanguageFeature parses a LanguageFeature from a string.
+/// @param str the string to parse
+/// @returns the parsed enum, or LanguageFeature::kUndefined if the string could not be parsed.
+LanguageFeature ParseLanguageFeature(std::string_view str) {
+    if (str == "readonly_and_readwrite_storage_textures") {
+        return LanguageFeature::kReadonlyAndReadwriteStorageTextures;
+    }
+    return LanguageFeature::kUndefined;
+}
+
+std::string_view ToString(LanguageFeature value) {
+    switch (value) {
+        case LanguageFeature::kUndefined:
+            return "undefined";
+        case LanguageFeature::kReadonlyAndReadwriteStorageTextures:
+            return "readonly_and_readwrite_storage_textures";
+    }
+    return "<unknown>";
+}
+
+}  // namespace tint::wgsl
diff --git a/src/tint/lang/wgsl/language_feature.cc.tmpl b/src/tint/lang/wgsl/language_feature.cc.tmpl
new file mode 100644
index 0000000..782b515
--- /dev/null
+++ b/src/tint/lang/wgsl/language_feature.cc.tmpl
@@ -0,0 +1,23 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/src/cmd/gen to generate language_feature.cc
+
+See:
+* tools/src/cmd/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+{{- $I := LoadIntrinsics "src/tint/lang/wgsl/wgsl.def" -}}
+{{- Import "src/tint/utils/templates/enums.tmpl.inc" -}}
+{{- $enum := ($I.Sem.Enum "language_feature") -}}
+
+#include "src/tint/lang/wgsl/language_feature.h"
+
+namespace tint::wgsl {
+
+{{ Eval "ParseEnum" $enum}}
+
+{{ Eval "EnumOStream" $enum}}
+
+}  // namespace tint::wgsl
diff --git a/src/tint/lang/wgsl/language_feature.h b/src/tint/lang/wgsl/language_feature.h
new file mode 100644
index 0000000..0a5573a
--- /dev/null
+++ b/src/tint/lang/wgsl/language_feature.h
@@ -0,0 +1,78 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+////////////////////////////////////////////////////////////////////////////////
+// File generated by 'tools/src/cmd/gen' using the template:
+//   src/tint/lang/wgsl/language_feature.h.tmpl
+//
+// To regenerate run: './tools/run gen'
+//
+//                       Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef SRC_TINT_LANG_WGSL_LANGUAGE_FEATURE_H_
+#define SRC_TINT_LANG_WGSL_LANGUAGE_FEATURE_H_
+
+#include "src/tint/utils/containers/unique_vector.h"
+#include "src/tint/utils/traits/traits.h"
+
+namespace tint::wgsl {
+
+/// An enumerator of WGSL language features
+/// @see src/tint/lang/wgsl/intrinsics.def for language feature descriptions
+enum class LanguageFeature : uint8_t {
+    kUndefined,
+    kReadonlyAndReadwriteStorageTextures,
+};
+
+/// @param value the enum value
+/// @returns the string for the given enum value
+std::string_view ToString(LanguageFeature value);
+
+/// @param out the stream to write to
+/// @param value the LanguageFeature
+/// @returns @p out so calls can be chained
+template <typename STREAM, typename = traits::EnableIfIsOStream<STREAM>>
+auto& operator<<(STREAM& out, LanguageFeature value) {
+    return out << ToString(value);
+}
+
+/// ParseLanguageFeature parses a LanguageFeature from a string.
+/// @param str the string to parse
+/// @returns the parsed enum, or LanguageFeature::kUndefined if the string could not be parsed.
+LanguageFeature ParseLanguageFeature(std::string_view str);
+
+constexpr const char* kLanguageFeatureStrings[] = {
+    "readonly_and_readwrite_storage_textures",
+};
+
+// A unique vector of language features
+using LanguageFeatures = UniqueVector<LanguageFeature, 4>;
+
+}  // namespace tint::wgsl
+
+#endif  // SRC_TINT_LANG_WGSL_LANGUAGE_FEATURE_H_
diff --git a/src/tint/lang/wgsl/language_feature.h.tmpl b/src/tint/lang/wgsl/language_feature.h.tmpl
new file mode 100644
index 0000000..cbe9f78
--- /dev/null
+++ b/src/tint/lang/wgsl/language_feature.h.tmpl
@@ -0,0 +1,32 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/src/cmd/gen to generate language_feature.h
+
+See:
+* tools/src/cmd/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+{{- $I := LoadIntrinsics "src/tint/lang/wgsl/wgsl.def" -}}
+{{- Import "src/tint/utils/templates/enums.tmpl.inc" -}}
+{{- $enum := ($I.Sem.Enum "language_feature") -}}
+
+#ifndef SRC_TINT_LANG_WGSL_LANGUAGE_FEATURE_H_
+#define SRC_TINT_LANG_WGSL_LANGUAGE_FEATURE_H_
+
+#include "src/tint/utils/traits/traits.h"
+#include "src/tint/utils/containers/unique_vector.h"
+
+namespace tint::wgsl {
+
+/// An enumerator of WGSL language features
+/// @see src/tint/lang/wgsl/intrinsics.def for language feature descriptions
+{{ Eval "DeclareEnum" $enum}}
+
+// A unique vector of language features
+using LanguageFeatures = UniqueVector<LanguageFeature, 4>;
+
+}  // namespace tint::wgsl
+
+#endif  // SRC_TINT_LANG_WGSL_LANGUAGE_FEATURE_H_
diff --git a/src/tint/lang/wgsl/language_feature_test.cc b/src/tint/lang/wgsl/language_feature_test.cc
new file mode 100644
index 0000000..4597ce9
--- /dev/null
+++ b/src/tint/lang/wgsl/language_feature_test.cc
@@ -0,0 +1,94 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+////////////////////////////////////////////////////////////////////////////////
+// File generated by 'tools/src/cmd/gen' using the template:
+//   src/tint/lang/wgsl/language_feature_test.cc.tmpl
+//
+// To regenerate run: './tools/run gen'
+//
+//                       Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
+#include "src/tint/lang/wgsl/language_feature.h"
+
+#include <gtest/gtest.h>
+
+#include <string>
+
+#include "src/tint/utils/text/string.h"
+
+namespace tint::wgsl {
+namespace {
+
+namespace parse_print_tests {
+
+struct Case {
+    const char* string;
+    LanguageFeature value;
+};
+
+inline std::ostream& operator<<(std::ostream& out, Case c) {
+    return out << "'" << std::string(c.string) << "'";
+}
+
+static constexpr Case kValidCases[] = {
+    {"readonly_and_readwrite_storage_textures",
+     LanguageFeature::kReadonlyAndReadwriteStorageTextures},
+};
+
+static constexpr Case kInvalidCases[] = {
+    {"eadonly_and_readwrite_stccrage_textures", LanguageFeature::kUndefined},
+    {"rladonly_a3readrite_storage_textures", LanguageFeature::kUndefined},
+    {"readonly_and_readwriVe_storage_textures", LanguageFeature::kUndefined},
+};
+
+using LanguageFeatureParseTest = testing::TestWithParam<Case>;
+
+TEST_P(LanguageFeatureParseTest, Parse) {
+    const char* string = GetParam().string;
+    LanguageFeature expect = GetParam().value;
+    EXPECT_EQ(expect, ParseLanguageFeature(string));
+}
+
+INSTANTIATE_TEST_SUITE_P(ValidCases, LanguageFeatureParseTest, testing::ValuesIn(kValidCases));
+INSTANTIATE_TEST_SUITE_P(InvalidCases, LanguageFeatureParseTest, testing::ValuesIn(kInvalidCases));
+
+using LanguageFeaturePrintTest = testing::TestWithParam<Case>;
+
+TEST_P(LanguageFeaturePrintTest, Print) {
+    LanguageFeature value = GetParam().value;
+    const char* expect = GetParam().string;
+    EXPECT_EQ(expect, tint::ToString(value));
+}
+
+INSTANTIATE_TEST_SUITE_P(ValidCases, LanguageFeaturePrintTest, testing::ValuesIn(kValidCases));
+
+}  // namespace parse_print_tests
+
+}  // namespace
+}  // namespace tint::wgsl
diff --git a/src/tint/lang/wgsl/language_feature_test.cc.tmpl b/src/tint/lang/wgsl/language_feature_test.cc.tmpl
new file mode 100644
index 0000000..fe98798
--- /dev/null
+++ b/src/tint/lang/wgsl/language_feature_test.cc.tmpl
@@ -0,0 +1,29 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/src/cmd/gen to generate language_feature_test.cc
+
+See:
+* tools/src/cmd/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+{{- $I := LoadIntrinsics "src/tint/lang/wgsl/wgsl.def" -}}
+{{- Import "src/tint/utils/templates/enums.tmpl.inc" -}}
+{{- $enum := ($I.Sem.Enum "language_feature") -}}
+
+#include "src/tint/lang/wgsl/language_feature.h"
+
+#include <gtest/gtest.h>
+
+#include <string>
+
+#include "src/tint/utils/text/string.h"
+
+namespace tint::wgsl {
+namespace {
+
+{{ Eval "TestParsePrintEnum" $enum}}
+
+}  // namespace
+}  // namespace tint::wgsl
diff --git a/src/tint/lang/wgsl/program/BUILD.bazel b/src/tint/lang/wgsl/program/BUILD.bazel
index 0cc90fa..1906aad 100644
--- a/src/tint/lang/wgsl/program/BUILD.bazel
+++ b/src/tint/lang/wgsl/program/BUILD.bazel
@@ -111,3 +111,21 @@
   visibility = ["//visibility:public"],
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+
diff --git a/src/tint/lang/wgsl/program/BUILD.cmake b/src/tint/lang/wgsl/program/BUILD.cmake
index 07c09e0..eea928d 100644
--- a/src/tint/lang/wgsl/program/BUILD.cmake
+++ b/src/tint/lang/wgsl/program/BUILD.cmake
@@ -109,3 +109,54 @@
 tint_target_add_external_dependencies(tint_lang_wgsl_program_test test
   "gtest"
 )
+
+################################################################################
+# Target:    tint_lang_wgsl_program_fuzz
+# Kind:      fuzz
+################################################################################
+tint_add_target(tint_lang_wgsl_program_fuzz fuzz
+)
+
+tint_target_add_dependencies(tint_lang_wgsl_program_fuzz fuzz
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  tint_lang_wgsl
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_program
+  tint_lang_wgsl_resolver
+  tint_lang_wgsl_sem
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_wgsl_program_fuzz fuzz
+    tint_cmd_fuzz_wgsl_fuzz
+    tint_lang_wgsl_reader_parser
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+if(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+  tint_target_add_sources(tint_lang_wgsl_program_fuzz fuzz
+    "lang/wgsl/program/clone_context_fuzz.cc"
+  )
+endif(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_wgsl_program_fuzz fuzz
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
diff --git a/src/tint/lang/wgsl/program/BUILD.gn b/src/tint/lang/wgsl/program/BUILD.gn
index 6cf7d8b..5641e61 100644
--- a/src/tint/lang/wgsl/program/BUILD.gn
+++ b/src/tint/lang/wgsl/program/BUILD.gn
@@ -109,3 +109,46 @@
     ]
   }
 }
+
+tint_fuzz_source_set("fuzz") {
+  sources = []
+  deps = [
+    "${tint_src_dir}/api/common",
+    "${tint_src_dir}/lang/core",
+    "${tint_src_dir}/lang/core/constant",
+    "${tint_src_dir}/lang/core/type",
+    "${tint_src_dir}/lang/wgsl",
+    "${tint_src_dir}/lang/wgsl/ast",
+    "${tint_src_dir}/lang/wgsl/program",
+    "${tint_src_dir}/lang/wgsl/resolver",
+    "${tint_src_dir}/lang/wgsl/sem",
+    "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/diagnostic",
+    "${tint_src_dir}/utils/ice",
+    "${tint_src_dir}/utils/id",
+    "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/math",
+    "${tint_src_dir}/utils/memory",
+    "${tint_src_dir}/utils/reflection",
+    "${tint_src_dir}/utils/result",
+    "${tint_src_dir}/utils/rtti",
+    "${tint_src_dir}/utils/symbol",
+    "${tint_src_dir}/utils/text",
+    "${tint_src_dir}/utils/traits",
+  ]
+
+  if (tint_build_wgsl_reader) {
+    deps += [
+      "${tint_src_dir}/cmd/fuzz/wgsl:fuzz",
+      "${tint_src_dir}/lang/wgsl/reader/parser",
+    ]
+  }
+
+  if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
+    sources += [ "clone_context_fuzz.cc" ]
+  }
+
+  if (tint_build_wgsl_writer) {
+    deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+  }
+}
diff --git a/src/tint/lang/wgsl/program/clone_context_fuzz.cc b/src/tint/lang/wgsl/program/clone_context_fuzz.cc
new file mode 100644
index 0000000..e4cfc2f
--- /dev/null
+++ b/src/tint/lang/wgsl/program/clone_context_fuzz.cc
@@ -0,0 +1,94 @@
+// Copyright 2020 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// GEN_BUILD:CONDITION(tint_build_wgsl_reader && tint_build_wgsl_writer)
+
+#include <string>
+#include <unordered_set>
+
+#include "src/tint/cmd/fuzz/wgsl/wgsl_fuzz.h"
+#include "src/tint/lang/wgsl/reader/parser/parser.h"
+#include "src/tint/lang/wgsl/writer/writer.h"
+
+namespace tint::program {
+
+#define ASSERT_EQ(A, B)                                         \
+    do {                                                        \
+        decltype(A) assert_a = (A);                             \
+        decltype(B) assert_b = (B);                             \
+        if (assert_a != assert_b) {                             \
+            TINT_ICE() << "ASSERT_EQ(" #A ", " #B ") failed:\n" \
+                       << #A << " was: " << assert_a << "\n"    \
+                       << #B << " was: " << assert_b << "\n";   \
+        }                                                       \
+    } while (false)
+
+#define ASSERT_TRUE(A)                                                                           \
+    do {                                                                                         \
+        decltype(A) assert_a = (A);                                                              \
+        if (!assert_a) {                                                                         \
+            TINT_ICE() << "ASSERT_TRUE(" #A ") failed:\n" << #A << " was: " << assert_a << "\n"; \
+        }                                                                                        \
+    } while (false)
+
+void CloneContextFuzzer(const tint::Program& src) {
+    // Clone the src program to dst
+    tint::Program dst(src.Clone());
+
+    // Expect the printed strings to match
+    ASSERT_EQ(tint::Program::printer(src), tint::Program::printer(dst));
+
+    // Check that none of the AST nodes or type pointers in dst are found in src
+    std::unordered_set<const tint::ast::Node*> src_nodes;
+    for (auto* src_node : src.ASTNodes().Objects()) {
+        src_nodes.emplace(src_node);
+    }
+    std::unordered_set<const tint::core::type::Type*> src_types;
+    for (auto* src_type : src.Types()) {
+        src_types.emplace(src_type);
+    }
+    for (auto* dst_node : dst.ASTNodes().Objects()) {
+        ASSERT_EQ(src_nodes.count(dst_node), 0u);
+    }
+    for (auto* dst_type : dst.Types()) {
+        ASSERT_EQ(src_types.count(dst_type), 0u);
+    }
+
+    tint::wgsl::writer::Options wgsl_options;
+
+    auto src_wgsl = tint::wgsl::writer::Generate(src, wgsl_options);
+    ASSERT_TRUE(src_wgsl);
+
+    auto dst_wgsl = tint::wgsl::writer::Generate(dst, wgsl_options);
+    ASSERT_TRUE(dst_wgsl);
+
+    ASSERT_EQ(src_wgsl->wgsl, dst_wgsl->wgsl);
+}
+
+}  // namespace tint::program
+
+TINT_WGSL_PROGRAM_FUZZER(tint::program::CloneContextFuzzer);
diff --git a/src/tint/lang/wgsl/reader/parser/parser.cc b/src/tint/lang/wgsl/reader/parser/parser.cc
index 40d1484..54aeb27 100644
--- a/src/tint/lang/wgsl/reader/parser/parser.cc
+++ b/src/tint/lang/wgsl/reader/parser/parser.cc
@@ -456,6 +456,7 @@
 //  : require identifier (COMMA identifier)* COMMA? SEMICOLON
 Maybe<Void> Parser::requires_directive() {
     return sync(Token::Type::kSemicolon, [&]() -> Maybe<Void> {
+        MultiTokenSource decl_source(this);
         if (!match(Token::Type::kRequires)) {
             return Failure::kNoMatch;
         }
@@ -473,34 +474,43 @@
             return add_error(t.source(), "requires directives don't take parenthesis");
         }
 
+        wgsl::LanguageFeatures features;
         while (continue_parsing()) {
-            auto& t2 = peek();
-
-            // Match the require name.
+            auto& t2 = next();
             if (handle_error(t2)) {
                 // The token might itself be an error.
                 return Failure::kErrored;
             }
 
+            // Match the require name.
             if (t2.IsIdentifier()) {
-                // TODO(dsinclair): When there are actual values for a requires directive they
-                // should be checked here.
-
-                // Any identifer is a valid feature name, so we correctly handle new feature
-                // names getting added in the future, they just all get flagged as not supported.
-                return add_error(t2.source(), "feature '" + t2.to_str() + "' is not supported");
-            }
-            if (t2.Is(Token::Type::kSemicolon)) {
-                break;
-            }
-            if (!match(Token::Type::kComma)) {
+                auto feature = wgsl::ParseLanguageFeature(t2.to_str_view());
+                if (feature == LanguageFeature::kUndefined) {
+                    // Any identifier is a valid feature name, so we correctly handle new feature
+                    // names getting added in the future, they just all get flagged as not
+                    // supported.
+                    return add_error(t2.source(), "feature '" + t2.to_str() + "' is not supported");
+                }
+                features.Add(feature);
+            } else {
                 return add_error(t2.source(), "invalid feature name for requires");
             }
+
+            if (!match(Token::Type::kComma)) {
+                break;
+            }
+            if (peek_is(Token::Type::kSemicolon)) {
+                break;
+            }
         }
-        // TODO(dsinclair): When there are actual values for a requires directive then the
-        // `while` will need to keep track if any were seen, and this needs to become
-        // conditional.
-        return add_error(t.source(), "missing feature names in requires directive");
+
+        if (!expect("requires directive", Token::Type::kSemicolon)) {
+            return Failure::kErrored;
+        }
+
+        builder_.AST().AddRequires(
+            create<ast::Requires>(decl_source.Source(), std::move(features)));
+        return kSuccess;
     });
 }
 
diff --git a/src/tint/lang/wgsl/reader/parser/require_directive_test.cc b/src/tint/lang/wgsl/reader/parser/require_directive_test.cc
index 43ea6be..8e7ff03 100644
--- a/src/tint/lang/wgsl/reader/parser/require_directive_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/require_directive_test.cc
@@ -33,11 +33,64 @@
 using RequiresDirectiveTest = WGSLParserTest;
 
 // Test a valid require directive.
-// There currently are no valid require directives
-TEST_F(RequiresDirectiveTest, DISABLED_Valid) {
-    auto p = parser("requires <sometime>;");
+TEST_F(RequiresDirectiveTest, Single) {
+    auto p = parser("requires readonly_and_readwrite_storage_textures;");
     p->requires_directive();
     EXPECT_FALSE(p->has_error()) << p->error();
+
+    auto program = p->program();
+    auto& ast = program.AST();
+    ASSERT_EQ(ast.Requires().Length(), 1u);
+    auto* req = ast.Requires()[0];
+    EXPECT_EQ(req->source.range.begin.line, 1u);
+    EXPECT_EQ(req->source.range.begin.column, 1u);
+    EXPECT_EQ(req->source.range.end.line, 1u);
+    EXPECT_EQ(req->source.range.end.column, 50u);
+    ASSERT_EQ(req->features.Length(), 1u);
+    EXPECT_EQ(req->features[0], wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures);
+    ASSERT_EQ(ast.GlobalDeclarations().Length(), 1u);
+    EXPECT_EQ(ast.GlobalDeclarations()[0], req);
+}
+
+// Test a valid require directive with a trailing comma.
+TEST_F(RequiresDirectiveTest, Single_TrailingComma) {
+    auto p = parser("requires readonly_and_readwrite_storage_textures,;");
+    p->requires_directive();
+    EXPECT_FALSE(p->has_error()) << p->error();
+
+    auto program = p->program();
+    auto& ast = program.AST();
+    ASSERT_EQ(ast.Requires().Length(), 1u);
+    auto* req = ast.Requires()[0];
+    EXPECT_EQ(req->source.range.begin.line, 1u);
+    EXPECT_EQ(req->source.range.begin.column, 1u);
+    EXPECT_EQ(req->source.range.end.line, 1u);
+    EXPECT_EQ(req->source.range.end.column, 51u);
+    ASSERT_EQ(req->features.Length(), 1u);
+    EXPECT_EQ(req->features[0], wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures);
+    ASSERT_EQ(ast.GlobalDeclarations().Length(), 1u);
+    EXPECT_EQ(ast.GlobalDeclarations()[0], req);
+}
+
+TEST_F(RequiresDirectiveTest, Multiple_Repeated) {
+    auto p = parser(
+        "requires readonly_and_readwrite_storage_textures, "
+        "readonly_and_readwrite_storage_textures;");
+    p->requires_directive();
+    EXPECT_FALSE(p->has_error()) << p->error();
+
+    auto program = p->program();
+    auto& ast = program.AST();
+    ASSERT_EQ(ast.Requires().Length(), 1u);
+    auto* req = ast.Requires()[0];
+    EXPECT_EQ(req->source.range.begin.line, 1u);
+    EXPECT_EQ(req->source.range.begin.column, 1u);
+    EXPECT_EQ(req->source.range.end.line, 1u);
+    EXPECT_EQ(req->source.range.end.column, 91u);
+    ASSERT_EQ(req->features.Length(), 1u);
+    EXPECT_EQ(req->features[0], wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures);
+    ASSERT_EQ(ast.GlobalDeclarations().Length(), 1u);
+    EXPECT_EQ(ast.GlobalDeclarations()[0], req);
 }
 
 // Test an unknown require identifier.
@@ -76,7 +129,7 @@
         auto p = parser("requires;");
         p->translation_unit();
         EXPECT_TRUE(p->has_error());
-        EXPECT_EQ(p->error(), R"(1:9: missing feature names in requires directive)");
+        EXPECT_EQ(p->error(), R"(1:9: invalid feature name for requires)");
     }
 }
 
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
index cf38225..a0c10a8 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
@@ -86,6 +86,7 @@
 #include "src/tint/lang/wgsl/ast/member_accessor_expression.h"
 #include "src/tint/lang/wgsl/ast/override.h"
 #include "src/tint/lang/wgsl/ast/phony_expression.h"
+#include "src/tint/lang/wgsl/ast/requires.h"
 #include "src/tint/lang/wgsl/ast/return_statement.h"
 #include "src/tint/lang/wgsl/ast/statement.h"
 #include "src/tint/lang/wgsl/ast/struct.h"
@@ -259,6 +260,9 @@
                 [&](const ast::DiagnosticDirective*) {
                     // Ignored for now.
                 },  //
+                [&](const ast::Requires*) {
+                    // Ignored for now.
+                },  //
                 TINT_ICE_ON_NO_MATCH);
         }
 
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir_test.cc
index 19d448b..f704fa5 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir_test.cc
@@ -1142,5 +1142,27 @@
 )");
 }
 
+TEST_F(IR_FromProgramTest, Requires) {
+    Require(wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures);
+    Func("f", tint::Empty, ty.void_(), tint::Empty);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << m;
+
+    ASSERT_EQ(1u, m->functions.Length());
+
+    auto* f = m->functions[0];
+    ASSERT_NE(f->Block(), nullptr);
+
+    EXPECT_EQ(m->functions[0]->Stage(), core::ir::Function::PipelineStage::kUndefined);
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%f = func():void -> %b1 {
+  %b1 = block {
+    ret
+  }
+}
+)");
+}
+
 }  // namespace
 }  // namespace tint::wgsl::reader
diff --git a/src/tint/lang/wgsl/resolver/BUILD.bazel b/src/tint/lang/wgsl/resolver/BUILD.bazel
index 719635c..578ead6 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.bazel
+++ b/src/tint/lang/wgsl/resolver/BUILD.bazel
@@ -123,6 +123,7 @@
     "inferred_type_test.cc",
     "is_host_shareable_test.cc",
     "is_storeable_test.cc",
+    "language_features_test.cc",
     "load_test.cc",
     "materialize_test.cc",
     "override_test.cc",
diff --git a/src/tint/lang/wgsl/resolver/BUILD.cmake b/src/tint/lang/wgsl/resolver/BUILD.cmake
index 75280ee..63026bf 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.cmake
+++ b/src/tint/lang/wgsl/resolver/BUILD.cmake
@@ -121,6 +121,7 @@
   lang/wgsl/resolver/inferred_type_test.cc
   lang/wgsl/resolver/is_host_shareable_test.cc
   lang/wgsl/resolver/is_storeable_test.cc
+  lang/wgsl/resolver/language_features_test.cc
   lang/wgsl/resolver/load_test.cc
   lang/wgsl/resolver/materialize_test.cc
   lang/wgsl/resolver/override_test.cc
diff --git a/src/tint/lang/wgsl/resolver/BUILD.gn b/src/tint/lang/wgsl/resolver/BUILD.gn
index b4b367f..86dc084 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.gn
+++ b/src/tint/lang/wgsl/resolver/BUILD.gn
@@ -123,6 +123,7 @@
       "inferred_type_test.cc",
       "is_host_shareable_test.cc",
       "is_storeable_test.cc",
+      "language_features_test.cc",
       "load_test.cc",
       "materialize_test.cc",
       "override_test.cc",
diff --git a/src/tint/lang/wgsl/resolver/dependency_graph.cc b/src/tint/lang/wgsl/resolver/dependency_graph.cc
index a261629..dda9251 100644
--- a/src/tint/lang/wgsl/resolver/dependency_graph.cc
+++ b/src/tint/lang/wgsl/resolver/dependency_graph.cc
@@ -201,6 +201,9 @@
             [&](const ast::Enable*) {
                 // Enable directives do not affect the dependency graph.
             },
+            [&](const ast::Requires*) {
+                // Requires directives do not affect the dependency graph.
+            },
             [&](const ast::ConstAssert* assertion) {
                 TraverseExpression(assertion->condition);
             },  //
@@ -611,6 +614,7 @@
             [&](const ast::Variable* var) { return var->name->symbol; },
             [&](const ast::DiagnosticDirective*) { return Symbol(); },
             [&](const ast::Enable*) { return Symbol(); },
+            [&](const ast::Requires*) { return Symbol(); },
             [&](const ast::ConstAssert*) { return Symbol(); },  //
             TINT_ICE_ON_NO_MATCH);
     }
@@ -714,13 +718,13 @@
 
         // Make sure all directives go before any other global declarations.
         for (auto* global : declaration_order_) {
-            if (global->node->IsAnyOf<ast::DiagnosticDirective, ast::Enable>()) {
+            if (global->node->IsAnyOf<ast::DiagnosticDirective, ast::Enable, ast::Requires>()) {
                 sorted_.Add(global->node);
             }
         }
 
         for (auto* global : declaration_order_) {
-            if (global->node->IsAnyOf<ast::DiagnosticDirective, ast::Enable>()) {
+            if (global->node->IsAnyOf<ast::DiagnosticDirective, ast::Enable, ast::Requires>()) {
                 // Skip directives here, as they are already added.
                 continue;
             }
diff --git a/src/tint/lang/wgsl/resolver/dependency_graph_test.cc b/src/tint/lang/wgsl/resolver/dependency_graph_test.cc
index 160df59..8a1f136 100644
--- a/src/tint/lang/wgsl/resolver/dependency_graph_test.cc
+++ b/src/tint/lang/wgsl/resolver/dependency_graph_test.cc
@@ -1122,9 +1122,12 @@
     auto* enable = Enable(wgsl::Extension::kF16);
     auto* var_2 = GlobalVar("SYMBOL2", ty.f32());
     auto* diagnostic = DiagnosticDirective(wgsl::DiagnosticSeverity::kWarning, "foo");
+    auto* var_3 = GlobalVar("SYMBOL3", ty.u32());
+    auto* req = Require(wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures);
 
-    EXPECT_THAT(AST().GlobalDeclarations(), ElementsAre(var_1, enable, var_2, diagnostic));
-    EXPECT_THAT(Build().ordered_globals, ElementsAre(enable, diagnostic, var_1, var_2));
+    EXPECT_THAT(AST().GlobalDeclarations(),
+                ElementsAre(var_1, enable, var_2, diagnostic, var_3, req));
+    EXPECT_THAT(Build().ordered_globals, ElementsAre(enable, diagnostic, req, var_1, var_2, var_3));
 }
 }  // namespace ordered_globals
 
diff --git a/src/tint/fuzzers/tint_wgsl_reader_wgsl_writer_fuzzer.cc b/src/tint/lang/wgsl/resolver/language_features_test.cc
similarity index 69%
copy from src/tint/fuzzers/tint_wgsl_reader_wgsl_writer_fuzzer.cc
copy to src/tint/lang/wgsl/resolver/language_features_test.cc
index 7ea3510..47a1990 100644
--- a/src/tint/fuzzers/tint_wgsl_reader_wgsl_writer_fuzzer.cc
+++ b/src/tint/lang/wgsl/resolver/language_features_test.cc
@@ -1,4 +1,4 @@
-// Copyright 2021 The Dawn & Tint Authors
+// Copyright 2023 The Dawn & Tint Authors
 //
 // Redistribution and use in source and binary forms, with or without
 // modification, are permitted provided that the following conditions are met:
@@ -25,19 +25,24 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#include <string>
+#include "src/tint/lang/wgsl/resolver/resolver.h"
 
-#include "src/tint/fuzzers/fuzzer_init.h"
-#include "src/tint/fuzzers/tint_reader_writer_fuzzer.h"
+#include "gmock/gmock.h"
+#include "src/tint/lang/core/fluent_types.h"
+#include "src/tint/lang/wgsl/resolver/resolver_helper_test.h"
 
-namespace tint::fuzzers {
+using namespace tint::core::number_suffixes;  // NOLINT
+using namespace tint::core::fluent_types;     // NOLINT
 
-extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
-    tint::fuzzers::ReaderWriterFuzzer fuzzer(InputFormat::kWGSL, OutputFormat::kWGSL);
-    fuzzer.SetDumpInput(GetCliParams().dump_input);
-    fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+namespace tint::resolver {
+namespace {
 
-    return fuzzer.Run(data, size);
+using ResolverLanguageFeaturesTest = ResolverTest;
+
+TEST_F(ResolverLanguageFeaturesTest, Requires) {
+    Require(wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures);
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
 
-}  // namespace tint::fuzzers
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/lang/wgsl/resolver/resolver.cc b/src/tint/lang/wgsl/resolver/resolver.cc
index 8ea7220..33b4e81 100644
--- a/src/tint/lang/wgsl/resolver/resolver.cc
+++ b/src/tint/lang/wgsl/resolver/resolver.cc
@@ -189,6 +189,7 @@
                     return DiagnosticControl(d->control);
                 },
                 [&](const ast::Enable* e) { return Enable(e); },
+                [&](const ast::Requires* r) { return Requires(r); },
                 [&](const ast::TypeDecl* td) { return TypeDecl(td); },
                 [&](const ast::Function* func) { return Function(func); },
                 [&](const ast::Variable* var) { return GlobalVariable(var); },
@@ -3958,6 +3959,11 @@
     return true;
 }
 
+bool Resolver::Requires(const ast::Requires*) {
+    // TODO(crbug.com/tint/2081): Check that all features are allowed.
+    return true;
+}
+
 core::type::Type* Resolver::TypeDecl(const ast::TypeDecl* named_type) {
     Mark(named_type->name);
 
diff --git a/src/tint/lang/wgsl/resolver/resolver.h b/src/tint/lang/wgsl/resolver/resolver.h
index fa353d4..887b117 100644
--- a/src/tint/lang/wgsl/resolver/resolver.h
+++ b/src/tint/lang/wgsl/resolver/resolver.h
@@ -466,9 +466,13 @@
     bool DiagnosticControl(const ast::DiagnosticControl& control);
 
     /// @param enable the enable declaration
-    /// @returns the resolved extension
+    /// @returns true on success, false on failure
     bool Enable(const ast::Enable* enable);
 
+    /// @param req the requires declaration
+    /// @returns true on success, false on failure
+    bool Requires(const ast::Requires* req);
+
     /// @param named_type the named type to resolve
     /// @returns the resolved semantic type
     core::type::Type* TypeDecl(const ast::TypeDecl* named_type);
diff --git a/src/tint/lang/wgsl/wgsl.def b/src/tint/lang/wgsl/wgsl.def
index 06039b2..a66c2ba 100644
--- a/src/tint/lang/wgsl/wgsl.def
+++ b/src/tint/lang/wgsl/wgsl.def
@@ -90,6 +90,11 @@
   chromium_experimental_pixel_local
 }
 
+// https://gpuweb.github.io/gpuweb/wgsl/#language-extensions-sec
+enum language_feature {
+  readonly_and_readwrite_storage_textures
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // WGSL primitive types                                                       //
 // Types may be decorated with @precedence(N) to prioritize which type        //
diff --git a/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel b/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel
index 16f6447..92a7f93 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel
+++ b/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel
@@ -99,6 +99,7 @@
     "literal_test.cc",
     "loop_test.cc",
     "member_accessor_test.cc",
+    "requires_test.cc",
     "return_test.cc",
     "switch_test.cc",
     "type_test.cc",
@@ -142,6 +143,11 @@
 )
 
 alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+alias(
   name = "tint_build_wgsl_writer",
   actual = "//src/tint:tint_build_wgsl_writer_true",
 )
diff --git a/src/tint/lang/wgsl/writer/ast_printer/BUILD.cmake b/src/tint/lang/wgsl/writer/ast_printer/BUILD.cmake
index b65e2a0..a3abc50 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/BUILD.cmake
+++ b/src/tint/lang/wgsl/writer/ast_printer/BUILD.cmake
@@ -102,6 +102,7 @@
   lang/wgsl/writer/ast_printer/literal_test.cc
   lang/wgsl/writer/ast_printer/loop_test.cc
   lang/wgsl/writer/ast_printer/member_accessor_test.cc
+  lang/wgsl/writer/ast_printer/requires_test.cc
   lang/wgsl/writer/ast_printer/return_test.cc
   lang/wgsl/writer/ast_printer/switch_test.cc
   lang/wgsl/writer/ast_printer/type_test.cc
@@ -146,4 +147,52 @@
   )
 endif(TINT_BUILD_WGSL_WRITER)
 
+endif(TINT_BUILD_WGSL_WRITER)
+if(TINT_BUILD_WGSL_WRITER)
+################################################################################
+# Target:    tint_lang_wgsl_writer_ast_printer_fuzz
+# Kind:      fuzz
+# Condition: TINT_BUILD_WGSL_WRITER
+################################################################################
+tint_add_target(tint_lang_wgsl_writer_ast_printer_fuzz fuzz
+)
+
+tint_target_add_dependencies(tint_lang_wgsl_writer_ast_printer_fuzz fuzz
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  tint_lang_wgsl
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_program
+  tint_lang_wgsl_sem
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_generator
+  tint_utils_ice
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_sources(tint_lang_wgsl_writer_ast_printer_fuzz fuzz
+    "lang/wgsl/writer/ast_printer/ast_printer_fuzz.cc"
+  )
+  tint_target_add_dependencies(tint_lang_wgsl_writer_ast_printer_fuzz fuzz
+    tint_cmd_fuzz_wgsl_fuzz
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_wgsl_writer_ast_printer_fuzz fuzz
+    tint_lang_wgsl_writer_ast_printer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
 endif(TINT_BUILD_WGSL_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/wgsl/writer/ast_printer/BUILD.gn b/src/tint/lang/wgsl/writer/ast_printer/BUILD.gn
index 0f53128..ab5e562 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/wgsl/writer/ast_printer/BUILD.gn
@@ -101,6 +101,7 @@
         "literal_test.cc",
         "loop_test.cc",
         "member_accessor_test.cc",
+        "requires_test.cc",
         "return_test.cc",
         "switch_test.cc",
         "type_test.cc",
@@ -141,3 +142,39 @@
     }
   }
 }
+if (tint_build_wgsl_writer) {
+  tint_fuzz_source_set("fuzz") {
+    sources = []
+    deps = [
+      "${tint_src_dir}/lang/core",
+      "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/type",
+      "${tint_src_dir}/lang/wgsl",
+      "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/program",
+      "${tint_src_dir}/lang/wgsl/sem",
+      "${tint_src_dir}/utils/containers",
+      "${tint_src_dir}/utils/diagnostic",
+      "${tint_src_dir}/utils/generator",
+      "${tint_src_dir}/utils/ice",
+      "${tint_src_dir}/utils/id",
+      "${tint_src_dir}/utils/macros",
+      "${tint_src_dir}/utils/math",
+      "${tint_src_dir}/utils/memory",
+      "${tint_src_dir}/utils/result",
+      "${tint_src_dir}/utils/rtti",
+      "${tint_src_dir}/utils/symbol",
+      "${tint_src_dir}/utils/text",
+      "${tint_src_dir}/utils/traits",
+    ]
+
+    if (tint_build_wgsl_reader) {
+      sources += [ "ast_printer_fuzz.cc" ]
+      deps += [ "${tint_src_dir}/cmd/fuzz/wgsl:fuzz" ]
+    }
+
+    if (tint_build_wgsl_writer) {
+      deps += [ "${tint_src_dir}/lang/wgsl/writer/ast_printer" ]
+    }
+  }
+}
diff --git a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
index 7ff0c0d..ec38e3d 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
@@ -102,6 +102,10 @@
         EmitEnable(enable);
         has_directives = true;
     }
+    for (auto req : program_.AST().Requires()) {
+        EmitRequires(req);
+        has_directives = true;
+    }
     for (auto diagnostic : program_.AST().DiagnosticDirectives()) {
         auto out = Line();
         EmitDiagnosticControl(out, diagnostic->control);
@@ -113,7 +117,7 @@
     }
     // Generate global declarations in the order they appear in the module.
     for (auto* decl : program_.AST().GlobalDeclarations()) {
-        if (decl->IsAnyOf<ast::DiagnosticDirective, ast::Enable>()) {
+        if (decl->IsAnyOf<ast::DiagnosticDirective, ast::Enable, ast::Requires>()) {
             continue;
         }
         Switch(
@@ -148,6 +152,20 @@
     out << ";";
 }
 
+void ASTPrinter::EmitRequires(const ast::Requires* req) {
+    auto out = Line();
+    out << "requires ";
+    bool first = true;
+    for (auto feature : req->features) {
+        if (!first) {
+            out << ", ";
+        }
+        out << feature;
+        first = false;
+    }
+    out << ";";
+}
+
 void ASTPrinter::EmitTypeDecl(const ast::TypeDecl* ty) {
     Switch(
         ty,  //
diff --git a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.h b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.h
index 9fe6ca3..71bb09a 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.h
+++ b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.h
@@ -67,6 +67,7 @@
 class LiteralExpression;
 class LoopStatement;
 class MemberAccessorExpression;
+class Requires;
 class ReturnStatement;
 class Statement;
 class Statement;
@@ -104,6 +105,9 @@
     /// Handles generating an enable directive
     /// @param enable the enable node
     void EmitEnable(const ast::Enable* enable);
+    /// Handles generating a requires directive
+    /// @param req the requires node
+    void EmitRequires(const ast::Requires* req);
     /// Handles generating a declared type
     /// @param ty the declared type to generate
     void EmitTypeDecl(const ast::TypeDecl* ty);
diff --git a/src/tint/fuzzers/tint_wgsl_reader_wgsl_writer_fuzzer.cc b/src/tint/lang/wgsl/writer/ast_printer/ast_printer_fuzz.cc
similarity index 75%
rename from src/tint/fuzzers/tint_wgsl_reader_wgsl_writer_fuzzer.cc
rename to src/tint/lang/wgsl/writer/ast_printer/ast_printer_fuzz.cc
index 7ea3510..77022be 100644
--- a/src/tint/fuzzers/tint_wgsl_reader_wgsl_writer_fuzzer.cc
+++ b/src/tint/lang/wgsl/writer/ast_printer/ast_printer_fuzz.cc
@@ -25,19 +25,20 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#include <string>
+// GEN_BUILD:CONDITION(tint_build_wgsl_reader)
 
-#include "src/tint/fuzzers/fuzzer_init.h"
-#include "src/tint/fuzzers/tint_reader_writer_fuzzer.h"
+#include "src/tint/lang/wgsl/writer/ast_printer/ast_printer.h"
 
-namespace tint::fuzzers {
+#include "src/tint/cmd/fuzz/wgsl/wgsl_fuzz.h"
 
-extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
-    tint::fuzzers::ReaderWriterFuzzer fuzzer(InputFormat::kWGSL, OutputFormat::kWGSL);
-    fuzzer.SetDumpInput(GetCliParams().dump_input);
-    fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+namespace tint::wgsl::writer {
+namespace {
 
-    return fuzzer.Run(data, size);
+void ASTPrinterFuzzer(const tint::Program& program) {
+    ASTPrinter{program}.Generate();
 }
 
-}  // namespace tint::fuzzers
+}  // namespace
+}  // namespace tint::wgsl::writer
+
+TINT_WGSL_PROGRAM_FUZZER(tint::wgsl::writer::ASTPrinterFuzzer);
diff --git a/src/tint/lang/wgsl/writer/ast_printer/requires_test.cc b/src/tint/lang/wgsl/writer/ast_printer/requires_test.cc
new file mode 100644
index 0000000..1526fbb
--- /dev/null
+++ b/src/tint/lang/wgsl/writer/ast_printer/requires_test.cc
@@ -0,0 +1,66 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/writer/ast_printer/helper_test.h"
+
+#include "gmock/gmock.h"
+
+namespace tint::wgsl::writer {
+namespace {
+
+using WgslASTPrinterTest = TestHelper;
+
+TEST_F(WgslASTPrinterTest, Emit_Requires) {
+    auto* req = Require(wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures);
+
+    ASTPrinter& gen = Build();
+
+    gen.EmitRequires(req);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
+    EXPECT_EQ(gen.Result(), R"(requires readonly_and_readwrite_storage_textures;
+)");
+}
+
+// TODO(jrprice): Enable this once we have multiple language features.
+TEST_F(WgslASTPrinterTest, DISABLED_Emit_Requires_Multiple) {
+    auto* req = create<ast::Requires>(
+        wgsl::LanguageFeatures({wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures,
+                                wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures}));
+    AST().AddRequires(req);
+
+    ASTPrinter& gen = Build();
+
+    gen.EmitRequires(req);
+    EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
+    EXPECT_EQ(
+        gen.Result(),
+        R"(requires readonly_and_readwrite_storage_textures, readonly_and_readwrite_storage_textures;
+)");
+}
+
+}  // namespace
+}  // namespace tint::wgsl::writer