dawn::native: Implement wgslLanguageFeatures.

Adds support for Instance::HasWGSLLanguageFeature and
Instance::EnumerateWGSLLanguageFeatures based on the data provided by
Tint in src/tint/lang/wgsl/features/status.h. A toggle is added to (by
default) hide the "chromium_testing_*" language features.

Use these testing features in a unittests.

Make the device use the instance's list of enabled language features to
pass the AllowedWGSLFeatures to Tint's WGSL reader.

Bug: dawn:2260
Change-Id: Ia6de5f9daf7d5a4f04c017624c32cf149382c6be
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/162500
Reviewed-by: Ben Clayton <bclayton@google.com>
Reviewed-by: Austin Eng <enga@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
diff --git a/dawn.json b/dawn.json
index 8657d95..b182288 100644
--- a/dawn.json
+++ b/dawn.json
@@ -2052,6 +2052,20 @@
                     {"name": "options", "type": "request adapter options", "annotation": "const*", "optional": true, "no_default": true},
                     {"name": "callback info", "type": "request adapter callback info"}
                 ]
+            },
+            {
+                "name": "has WGSL language feature",
+                "returns": "bool",
+                "args": [
+                    {"name": "feature", "type": "WGSL feature name"}
+                ]
+            },
+            {
+                "name": "enumerate WGSL language features",
+                "returns": "size_t",
+                "args": [
+                    {"name": "features", "type": "WGSL feature name", "annotation": "*"}
+                ]
             }
         ]
     },
@@ -3664,6 +3678,21 @@
             {"value": 31, "name": "unorm 10_10_10_2", "jsrepr": "'unorm10-10-10-2'"}
         ]
     },
+    "WGSL feature name": {
+        "category": "enum",
+        "values": [
+            {"value": 0, "name": "undefined", "valid": false, "jsrepr": "undefined"},
+            {"value": 1, "name": "readonly and readwrite storage textures"},
+            {"value": 2, "name": "packed 4x8 integer dot product"},
+            {"value": 3, "name": "unrestricted pointer parameters"},
+            {"value": 4, "name": "pointer composite access"},
+            {"value": 1000, "name": "chromium testing unimplemented", "tags": ["dawn"]},
+            {"value": 1001, "name": "chromium testing unsafe experimental", "tags": ["dawn"]},
+            {"value": 1002, "name": "chromium testing experimental", "tags": ["dawn"]},
+            {"value": 1003, "name": "chromium testing shipped with killswitch", "tags": ["dawn"]},
+            {"value": 1004, "name": "chromium testing shipped", "tags": ["dawn"]}
+        ]
+    },
     "whole size" : {
         "category": "constant",
         "type": "uint64_t",
diff --git a/dawn_wire.json b/dawn_wire.json
index cc541ed..831f8be 100644
--- a/dawn_wire.json
+++ b/dawn_wire.json
@@ -216,6 +216,8 @@
             "DeviceSetDeviceLostCallback",
             "DeviceSetUncapturedErrorCallback",
             "DeviceSetLoggingCallback",
+            "InstanceEnumerateWGSLLanguageFeatures",
+            "InstanceHasWGSLLanguageFeature",
             "InstanceRequestAdapter",
             "InstanceRequestAdapterF",
             "ShaderModuleGetCompilationInfo",
diff --git a/generator/dawn_json_generator.py b/generator/dawn_json_generator.py
index 8f9ad3c..f9c900c 100644
--- a/generator/dawn_json_generator.py
+++ b/generator/dawn_json_generator.py
@@ -63,7 +63,7 @@
         return chunk[0].upper() + chunk[1:]
 
     def canonical_case(self):
-        return (' '.join(self.chunks)).lower()
+        return ' '.join(self.chunks)
 
     def concatcase(self):
         return ''.join(self.chunks)
diff --git a/src/dawn/common/WGSLFeatureMapping.h b/src/dawn/common/WGSLFeatureMapping.h
new file mode 100644
index 0000000..d570b82
--- /dev/null
+++ b/src/dawn/common/WGSLFeatureMapping.h
@@ -0,0 +1,40 @@
+// 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_DAWN_COMMON_WGSLFEATUREMAPPING_H_
+#define SRC_DAWN_COMMON_WGSLFEATUREMAPPING_H_
+
+#define DAWN_FOREACH_WGSL_FEATURE(X)                                               \
+    X(kUndefined, Undefined)                                                       \
+    X(kReadonlyAndReadwriteStorageTextures, ReadonlyAndReadwriteStorageTextures)   \
+    X(kChromiumTestingUnimplemented, ChromiumTestingUnimplemented)                 \
+    X(kChromiumTestingUnsafeExperimental, ChromiumTestingUnsafeExperimental)       \
+    X(kChromiumTestingExperimental, ChromiumTestingExperimental)                   \
+    X(kChromiumTestingShippedWithKillswitch, ChromiumTestingShippedWithKillswitch) \
+    X(kChromiumTestingShipped, ChromiumTestingShipped)
+
+#endif  // SRC_DAWN_COMMON_WGSLFEATUREMAPPING_H_
diff --git a/src/dawn/native/BUILD.gn b/src/dawn/native/BUILD.gn
index 497dc0d..487bcc0 100644
--- a/src/dawn/native/BUILD.gn
+++ b/src/dawn/native/BUILD.gn
@@ -159,6 +159,7 @@
     ":utils_gen",
     "${dawn_root}/src/dawn/common",
     "${dawn_root}/src/tint/api",
+    "${dawn_root}/src/tint/lang/wgsl/features",
 
     # TODO(dawn:286): These should only be necessary if SPIR-V validation is
     # enabled with dawn_enable_spirv_validation
diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp
index 0687856..6825209 100644
--- a/src/dawn/native/Device.cpp
+++ b/src/dawn/native/Device.cpp
@@ -1465,10 +1465,6 @@
     if (IsToggleEnabled(Toggle::AllowUnsafeAPIs)) {
         mWGSLAllowedFeatures.extensions.insert(
             tint::wgsl::Extension::kChromiumDisableUniformityAnalysis);
-
-        // Allow language features that are still under development.
-        mWGSLAllowedFeatures.features.insert(
-            tint::wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures);
     }
     if (mEnabledFeatures.IsEnabled(Feature::DualSourceBlending)) {
         mWGSLAllowedFeatures.extensions.insert(
@@ -1483,6 +1479,13 @@
         mWGSLAllowedFeatures.extensions.insert(
             tint::wgsl::Extension::kChromiumExperimentalFramebufferFetch);
     }
+
+    // Language features are enabled instance-wide.
+    // mAdapter is not set for mock test devices.
+    // TODO(crbug.com/dawn/1702): using a mock adapter and instance could avoid the null checking.
+    if (mAdapter != nullptr) {
+        mWGSLAllowedFeatures.features = GetInstance()->GetAllowedWGSLLanguageFeatures();
+    }
 }
 
 const tint::wgsl::AllowedFeatures& DeviceBase::GetWGSLAllowedFeatures() const {
diff --git a/src/dawn/native/Device.h b/src/dawn/native/Device.h
index e9ead1b..73890af 100644
--- a/src/dawn/native/Device.h
+++ b/src/dawn/native/Device.h
@@ -74,8 +74,6 @@
 struct ShaderModuleParseResult;
 struct TrackedFutureWaitInfo;
 
-using WGSLExtensionSet = std::unordered_set<std::string>;
-
 class DeviceBase : public RefCountedWithExternalCount {
   public:
     DeviceBase(AdapterBase* adapter,
diff --git a/src/dawn/native/Instance.cpp b/src/dawn/native/Instance.cpp
index 2a9069b..dac0627 100644
--- a/src/dawn/native/Instance.cpp
+++ b/src/dawn/native/Instance.cpp
@@ -34,6 +34,7 @@
 #include "dawn/common/GPUInfo.h"
 #include "dawn/common/Log.h"
 #include "dawn/common/SystemUtils.h"
+#include "dawn/common/WGSLFeatureMapping.h"
 #include "dawn/native/CallbackTaskManager.h"
 #include "dawn/native/ChainUtils.h"
 #include "dawn/native/Device.h"
@@ -42,6 +43,7 @@
 #include "dawn/native/Toggles.h"
 #include "dawn/native/ValidationUtils_autogen.h"
 #include "dawn/platform/DawnPlatform.h"
+#include "tint/lang/wgsl/features/status.h"
 
 // For SwiftShader fallback
 #if defined(DAWN_ENABLE_BACKEND_VULKAN)
@@ -106,6 +108,31 @@
     return nullptr;
 }
 
+wgpu::WGSLFeatureName ToWGPUFeature(tint::wgsl::LanguageFeature f) {
+    switch (f) {
+#define CASE(WgslName, WgpuName)                \
+    case tint::wgsl::LanguageFeature::WgslName: \
+        return wgpu::WGSLFeatureName::WgpuName;
+        DAWN_FOREACH_WGSL_FEATURE(CASE)
+#undef CASE
+    }
+}
+
+tint::wgsl::LanguageFeature ToWGSLFeature(wgpu::WGSLFeatureName f) {
+    switch (f) {
+#define CASE(WgslName, WgpuName)          \
+    case wgpu::WGSLFeatureName::WgpuName: \
+        return tint::wgsl::LanguageFeature::WgslName;
+        DAWN_FOREACH_WGSL_FEATURE(CASE)
+#undef CASE
+        case wgpu::WGSLFeatureName::Packed4x8IntegerDotProduct:
+        case wgpu::WGSLFeatureName::UnrestrictedPointerParameters:
+        case wgpu::WGSLFeatureName::PointerCompositeAccess:
+            return tint::wgsl::LanguageFeature::kUndefined;
+    }
+}
+DAWN_UNUSED_FUNC(ToWGSLFeature);
+
 }  // anonymous namespace
 
 wgpu::Bool APIGetInstanceFeatures(InstanceFeatures* features) {
@@ -222,6 +249,8 @@
 
     DAWN_TRY(mEventManager.Initialize(descriptor));
 
+    GatherWGSLFeatures();
+
     return {};
 }
 
@@ -548,4 +577,65 @@
     return new Surface(this, descriptor);
 }
 
+const std::unordered_set<tint::wgsl::LanguageFeature>&
+InstanceBase::GetAllowedWGSLLanguageFeatures() const {
+    return mTintLanguageFeatures;
+}
+
+void InstanceBase::GatherWGSLFeatures() {
+    for (auto wgslFeature : tint::wgsl::kAllLanguageFeatures) {
+        // Skip over testing features if we don't have the toggle to expose them.
+        if (!mToggles.IsEnabled(Toggle::ExposeWGSLTestingFeatures)) {
+            switch (wgslFeature) {
+                case tint::wgsl::LanguageFeature::kChromiumTestingUnimplemented:
+                case tint::wgsl::LanguageFeature::kChromiumTestingUnsafeExperimental:
+                case tint::wgsl::LanguageFeature::kChromiumTestingExperimental:
+                case tint::wgsl::LanguageFeature::kChromiumTestingShippedWithKillswitch:
+                case tint::wgsl::LanguageFeature::kChromiumTestingShipped:
+                    continue;
+                default:
+                    break;
+            }
+        }
+
+        // Expose the feature depending on its status and allow_unsafe_apis.
+        bool enable = false;
+        switch (tint::wgsl::GetLanguageFeatureStatus(wgslFeature)) {
+            case tint::wgsl::FeatureStatus::kUnknown:
+            case tint::wgsl::FeatureStatus::kUnimplemented:
+                enable = false;
+                break;
+
+            case tint::wgsl::FeatureStatus::kUnsafeExperimental:
+            case tint::wgsl::FeatureStatus::kExperimental:
+                enable = mToggles.IsEnabled(Toggle::AllowUnsafeAPIs);
+                break;
+
+            case tint::wgsl::FeatureStatus::kShippedWithKillswitch:
+            case tint::wgsl::FeatureStatus::kShipped:
+                enable = true;
+                break;
+        }
+
+        if (enable) {
+            mWGSLFeatures.emplace(ToWGPUFeature(wgslFeature));
+            mTintLanguageFeatures.emplace(wgslFeature);
+        }
+    }
+}
+
+bool InstanceBase::APIHasWGSLLanguageFeature(wgpu::WGSLFeatureName feature) const {
+    return mWGSLFeatures.count(feature) != 0;
+}
+
+size_t InstanceBase::APIEnumerateWGSLLanguageFeatures(wgpu::WGSLFeatureName* features) const {
+    if (features != nullptr) {
+        for (wgpu::WGSLFeatureName f : mWGSLFeatures) {
+            *features = f;
+            ++features;
+        }
+    }
+    return mWGSLFeatures.size();
+}
+
 }  // namespace dawn::native
diff --git a/src/dawn/native/Instance.h b/src/dawn/native/Instance.h
index 8a74906..2ec2d77 100644
--- a/src/dawn/native/Instance.h
+++ b/src/dawn/native/Instance.h
@@ -48,6 +48,7 @@
 #include "dawn/native/RefCountedWithExternalCount.h"
 #include "dawn/native/Toggles.h"
 #include "dawn/native/dawn_platform.h"
+#include "tint/lang/wgsl/features/language_feature.h"
 
 namespace dawn::platform {
 class Platform;
@@ -114,6 +115,7 @@
     }
 
     const TogglesState& GetTogglesState() const;
+    const std::unordered_set<tint::wgsl::LanguageFeature>& GetAllowedWGSLLanguageFeatures() const;
 
     // Used to query the details of a toggle. Return nullptr if toggleName is not a valid name
     // of a toggle supported in Dawn.
@@ -162,6 +164,10 @@
     [[nodiscard]] wgpu::WaitStatus APIWaitAny(size_t count,
                                               FutureWaitInfo* futures,
                                               uint64_t timeoutNS);
+    bool APIHasWGSLLanguageFeature(wgpu::WGSLFeatureName feature) const;
+    // Always writes the full list when features is not nullptr.
+    // TODO(https://github.com/webgpu-native/webgpu-headers/issues/252): Add a count argument.
+    size_t APIEnumerateWGSLLanguageFeatures(wgpu::WGSLFeatureName* features) const;
 
   private:
     explicit InstanceBase(const TogglesState& instanceToggles);
@@ -191,6 +197,7 @@
                                    const DawnTogglesDescriptor* requiredAdapterToggles,
                                    wgpu::PowerPreference powerPreference) const;
 
+    void GatherWGSLFeatures();
     void ConsumeError(std::unique_ptr<ErrorData> error);
 
     std::unordered_set<std::string> warningMessages;
@@ -212,6 +219,9 @@
     TogglesState mToggles;
     TogglesInfo mTogglesInfo;
 
+    std::unordered_set<wgpu::WGSLFeatureName> mWGSLFeatures;
+    std::unordered_set<tint::wgsl::LanguageFeature> mTintLanguageFeatures;
+
 #if defined(DAWN_USE_X11)
     std::unique_ptr<X11Functions> mX11Functions;
 #endif  // defined(DAWN_USE_X11)
diff --git a/src/dawn/native/Toggles.cpp b/src/dawn/native/Toggles.cpp
index b2b88e1..982b146 100644
--- a/src/dawn/native/Toggles.cpp
+++ b/src/dawn/native/Toggles.cpp
@@ -490,6 +490,11 @@
      {"polyfill_packed_4x8_dot_product",
       "Always use the polyfill version of dot4I8Packed() and dot4U8Packed().",
       "https://crbug.com/tint/1497", ToggleStage::Device}},
+    {Toggle::ExposeWGSLTestingFeatures,
+     {"expose_wgsl_testing_features",
+      "Make the Instance expose the ChromiumExperimental* features for testing of "
+      "wgslLanguageFeatures functionality.",
+      "https://crbug.com/dawn/2260", ToggleStage::Instance}},
     {Toggle::NoWorkaroundSampleMaskBecomesZeroForAllButLastColorTarget,
      {"no_workaround_sample_mask_becomes_zero_for_all_but_last_color_target",
       "MacOS 12.0+ Intel has a bug where the sample mask is only applied for the last color "
diff --git a/src/dawn/native/Toggles.h b/src/dawn/native/Toggles.h
index b1a125f..a262fed 100644
--- a/src/dawn/native/Toggles.h
+++ b/src/dawn/native/Toggles.h
@@ -123,6 +123,7 @@
     UseTintIR,
     D3DDisableIEEEStrictness,
     PolyFillPacked4x8DotProduct,
+    ExposeWGSLTestingFeatures,
 
     // Unresolved issues.
     NoWorkaroundSampleMaskBecomesZeroForAllButLastColorTarget,
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index e0c4040..6021591 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -409,6 +409,7 @@
     "unittests/validation/ValidationTest.h",
     "unittests/validation/VertexBufferValidationTests.cpp",
     "unittests/validation/VertexStateValidationTests.cpp",
+    "unittests/validation/WGSLFeatureValidationTests.cpp",
     "unittests/validation/WritableBufferBindingAliasingValidationTests.cpp",
     "unittests/validation/WritableTextureBindingAliasingValidationTests.cpp",
     "unittests/validation/WriteBufferTests.cpp",
diff --git a/src/dawn/tests/unittests/validation/StorageTextureValidationTests.cpp b/src/dawn/tests/unittests/validation/StorageTextureValidationTests.cpp
index ae154bf..c98996e 100644
--- a/src/dawn/tests/unittests/validation/StorageTextureValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/StorageTextureValidationTests.cpp
@@ -1181,18 +1181,40 @@
 }
 
 // Check that both read-only and read-write storage textures are validated as unsafe in render and
-// compute pipelines.
-TEST_F(ReadWriteStorageTextureDisallowUnsafeAPITests, ReadWriteStorageTextureInPipeline) {
+// compute pipelines when the layout is defaulted.
+TEST_F(ReadWriteStorageTextureDisallowUnsafeAPITests, ReadWriteStorageTextureInDefaultedLayout) {
     constexpr std::array<wgpu::StorageTextureAccess, 2> kStorageTextureAccesses = {
         {wgpu::StorageTextureAccess::ReadOnly, wgpu::StorageTextureAccess::ReadWrite}};
-    constexpr std::array<wgpu::ShaderStage, 3> kShaderStages = {
-        {wgpu::ShaderStage::Vertex, wgpu::ShaderStage::Fragment, wgpu::ShaderStage::Compute}};
 
     for (wgpu::StorageTextureAccess access : kStorageTextureAccesses) {
-        for (wgpu::ShaderStage shaderStage : kShaderStages) {
-            std::string shader = CreateShaderWithStorageTexture(
-                access, wgpu::TextureFormat::R32Float, "texture_storage_2d", shaderStage);
-            ASSERT_DEVICE_ERROR(utils::CreateShaderModule(device, shader.c_str()));
+        // Compute stage
+        {
+            wgpu::ComputePipelineDescriptor desc;
+            desc.compute.module = utils::CreateShaderModule(
+                device,
+                CreateShaderWithStorageTexture(access, wgpu::TextureFormat::R32Float,
+                                               "texture_storage_2d", wgpu::ShaderStage::Compute));
+            ASSERT_DEVICE_ERROR(device.CreateComputePipeline(&desc));
+        }
+        // Vertex stage
+        {
+            utils::ComboRenderPipelineDescriptor desc;
+            desc.vertex.module = utils::CreateShaderModule(
+                device,
+                CreateShaderWithStorageTexture(access, wgpu::TextureFormat::R32Float,
+                                               "texture_storage_2d", wgpu::ShaderStage::Vertex));
+            desc.cFragment.module = mDefaultFSModule;
+            ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&desc));
+        }
+        // Fragment stage
+        {
+            utils::ComboRenderPipelineDescriptor desc;
+            desc.cFragment.module = utils::CreateShaderModule(
+                device,
+                CreateShaderWithStorageTexture(access, wgpu::TextureFormat::R32Float,
+                                               "texture_storage_2d", wgpu::ShaderStage::Fragment));
+            desc.vertex.module = mDefaultVSModule;
+            ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&desc));
         }
     }
 }
diff --git a/src/dawn/tests/unittests/validation/ValidationTest.cpp b/src/dawn/tests/unittests/validation/ValidationTest.cpp
index f05cb50..4a5c9ff 100644
--- a/src/dawn/tests/unittests/validation/ValidationTest.cpp
+++ b/src/dawn/tests/unittests/validation/ValidationTest.cpp
@@ -123,8 +123,10 @@
     procs.adapterRequestDevice = [](WGPUAdapter self, const WGPUDeviceDescriptor* descriptor,
                                     WGPURequestDeviceCallback callback, void* userdata) {
         DAWN_ASSERT(gCurrentTest);
-        wgpu::DeviceDescriptor deviceDesc =
-            *(reinterpret_cast<const wgpu::DeviceDescriptor*>(descriptor));
+        wgpu::DeviceDescriptor deviceDesc = {};
+        if (descriptor != nullptr) {
+            deviceDesc = *(reinterpret_cast<const wgpu::DeviceDescriptor*>(descriptor));
+        }
         WGPUDevice cDevice = gCurrentTest->CreateTestDevice(
             dawn::native::Adapter(reinterpret_cast<dawn::native::AdapterBase*>(self)), deviceDesc);
         DAWN_ASSERT(cDevice != nullptr);
diff --git a/src/dawn/tests/unittests/validation/ValidationTest.h b/src/dawn/tests/unittests/validation/ValidationTest.h
index 0a8c203..7ce1364 100644
--- a/src/dawn/tests/unittests/validation/ValidationTest.h
+++ b/src/dawn/tests/unittests/validation/ValidationTest.h
@@ -170,6 +170,8 @@
                                         wgpu::DeviceDescriptor descriptor);
 
     wgpu::Device RequestDeviceSync(const wgpu::DeviceDescriptor& deviceDesc);
+    static void OnDeviceError(WGPUErrorType type, const char* message, void* userdata);
+    static void OnDeviceLost(WGPUDeviceLostReason reason, const char* message, void* userdata);
 
     virtual bool UseCompatibilityMode() const;
 
@@ -186,8 +188,6 @@
     std::unique_ptr<dawn::utils::WireHelper> mWireHelper;
     WGPUDevice mLastCreatedBackendDevice;
 
-    static void OnDeviceError(WGPUErrorType type, const char* message, void* userdata);
-    static void OnDeviceLost(WGPUDeviceLostReason reason, const char* message, void* userdata);
     std::string mDeviceErrorMessage;
     bool mExpectError = false;
     bool mError = false;
diff --git a/src/dawn/tests/unittests/validation/WGSLFeatureValidationTests.cpp b/src/dawn/tests/unittests/validation/WGSLFeatureValidationTests.cpp
new file mode 100644
index 0000000..f7e9707
--- /dev/null
+++ b/src/dawn/tests/unittests/validation/WGSLFeatureValidationTests.cpp
@@ -0,0 +1,228 @@
+// 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 <algorithm>
+#include <vector>
+
+#include "dawn/tests/unittests/validation/ValidationTest.h"
+#include "dawn/utils/WGPUHelpers.h"
+
+namespace dawn {
+namespace {
+
+class WGSLFeatureValidationTest : public ValidationTest {
+  protected:
+    void SetUp() override {
+        ValidationTest::SetUp();
+        DAWN_SKIP_TEST_IF(UsesWire());
+    }
+
+    struct InstanceSpec {
+        bool useTestingFeatures = true;
+        bool allowUnsafeAPIs = false;
+    };
+
+    wgpu::Instance CreateInstance(InstanceSpec spec) {
+        wgpu::InstanceDescriptor desc;
+
+        std::vector<const char*> enabledToggles;
+        if (spec.useTestingFeatures) {
+            enabledToggles.push_back("expose_wgsl_testing_features");
+        }
+        if (spec.allowUnsafeAPIs) {
+            enabledToggles.push_back("allow_unsafe_apis");
+        }
+
+        wgpu::DawnTogglesDescriptor togglesDesc;
+        togglesDesc.nextInChain = desc.nextInChain;
+        desc.nextInChain = &togglesDesc;
+        togglesDesc.enabledToggleCount = enabledToggles.size();
+        togglesDesc.enabledToggles = enabledToggles.data();
+
+        return wgpu::CreateInstance(&desc);
+    }
+
+    wgpu::Device CreateDeviceOnInstance(wgpu::Instance instance) {
+        // Get the adapter
+        wgpu::Adapter adapter;
+        instance.RequestAdapter(
+            nullptr,
+            [](WGPURequestAdapterStatus status, WGPUAdapter a, const char* message,
+               void* userdata) {
+                ASSERT_EQ(status, WGPURequestAdapterStatus_Success);
+                ASSERT_NE(a, nullptr);
+                *reinterpret_cast<wgpu::Adapter*>(userdata) = wgpu::Adapter::Acquire(a);
+            },
+            &adapter);
+
+        while (!adapter) {
+            FlushWire();
+        }
+        EXPECT_NE(nullptr, adapter.Get());
+
+        // Get the device
+        wgpu::Device device;
+        adapter.RequestDevice(
+            nullptr,
+            [](WGPURequestDeviceStatus status, WGPUDevice d, const char* message, void* userdata) {
+                ASSERT_EQ(status, WGPURequestDeviceStatus_Success);
+                ASSERT_NE(d, nullptr);
+                *reinterpret_cast<wgpu::Device*>(userdata) = wgpu::Device::Acquire(d);
+            },
+            &device);
+
+        while (!device) {
+            FlushWire();
+        }
+        EXPECT_NE(nullptr, device.Get());
+
+        device.SetUncapturedErrorCallback(ValidationTest::OnDeviceError, this);
+        return device;
+    }
+};
+
+wgpu::WGSLFeatureName kNonExistentFeature = static_cast<wgpu::WGSLFeatureName>(0xFFFF'FFFF);
+
+// Check HasFeature for an Instance that doesn't have unsafe APIs.
+TEST_F(WGSLFeatureValidationTest, HasFeatureDefaultInstance) {
+    wgpu::Instance instance = CreateInstance({});
+
+    // Shipped features are present.
+    ASSERT_TRUE(instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingShipped));
+    ASSERT_TRUE(instance.HasWGSLLanguageFeature(
+        wgpu::WGSLFeatureName::ChromiumTestingShippedWithKillswitch));
+
+    // Experimental and unimplemented features are not present.
+    ASSERT_FALSE(
+        instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingExperimental));
+    ASSERT_FALSE(
+        instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingUnsafeExperimental));
+    ASSERT_FALSE(
+        instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingUnimplemented));
+
+    // Non-existent features are not present.
+    ASSERT_FALSE(instance.HasWGSLLanguageFeature(kNonExistentFeature));
+}
+
+// Check HasFeature for an Instance that has unsafe APIs.
+TEST_F(WGSLFeatureValidationTest, HasFeatureAllowUnsafeInstance) {
+    wgpu::Instance instance = CreateInstance({.allowUnsafeAPIs = true});
+
+    // Shipped and experimental features are present.
+    ASSERT_TRUE(instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingShipped));
+    ASSERT_TRUE(instance.HasWGSLLanguageFeature(
+        wgpu::WGSLFeatureName::ChromiumTestingShippedWithKillswitch));
+    ASSERT_TRUE(
+        instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingExperimental));
+    ASSERT_TRUE(
+        instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingUnsafeExperimental));
+
+    // Experimental and unimplemented features are not present.
+    ASSERT_FALSE(
+        instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingUnimplemented));
+
+    // Non-existent features are not present.
+    ASSERT_FALSE(instance.HasWGSLLanguageFeature(kNonExistentFeature));
+}
+
+// Check HasFeature for an Instance that doesn't have the expose_wgsl_testing_features toggle.
+TEST_F(WGSLFeatureValidationTest, HasFeatureWithoutExposeWGSLTestingFeatures) {
+    wgpu::Instance instance = CreateInstance({.useTestingFeatures = false});
+
+    // None of the testing features are present.
+    ASSERT_FALSE(instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingShipped));
+    ASSERT_FALSE(instance.HasWGSLLanguageFeature(
+        wgpu::WGSLFeatureName::ChromiumTestingShippedWithKillswitch));
+    ASSERT_FALSE(
+        instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingExperimental));
+    ASSERT_FALSE(
+        instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingUnsafeExperimental));
+    ASSERT_FALSE(
+        instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingUnimplemented));
+}
+
+// Tests for the behavior of WGSL feature enumeration.
+TEST_F(WGSLFeatureValidationTest, EnumerateFeatures) {
+    wgpu::Instance instance = CreateInstance({});
+
+    size_t featureCount = instance.EnumerateWGSLLanguageFeatures(nullptr);
+
+    std::vector<wgpu::WGSLFeatureName> features(featureCount + 1, kNonExistentFeature);
+    size_t secondFeatureCount = instance.EnumerateWGSLLanguageFeatures(features.data());
+
+    // Exactly featureCount features should be written, and all return true in HasWGSLFeature.
+    ASSERT_EQ(secondFeatureCount, featureCount);
+    for (size_t i = 0; i < featureCount; i++) {
+        ASSERT_TRUE(instance.HasWGSLLanguageFeature(features[i]));
+        ASSERT_NE(kNonExistentFeature, features[i]);
+    }
+    ASSERT_EQ(kNonExistentFeature, features[featureCount]);
+
+    // Test the presence / absence of some known testing features.
+    ASSERT_NE(
+        std::find(features.begin(), features.end(), wgpu::WGSLFeatureName::ChromiumTestingShipped),
+        features.end());
+    ASSERT_NE(std::find(features.begin(), features.end(),
+                        wgpu::WGSLFeatureName::ChromiumTestingShippedWithKillswitch),
+              features.end());
+
+    ASSERT_EQ(std::find(features.begin(), features.end(),
+                        wgpu::WGSLFeatureName::ChromiumTestingUnimplemented),
+              features.end());
+    ASSERT_EQ(std::find(features.begin(), features.end(),
+                        wgpu::WGSLFeatureName::ChromiumTestingUnsafeExperimental),
+              features.end());
+    ASSERT_EQ(std::find(features.begin(), features.end(),
+                        wgpu::WGSLFeatureName::ChromiumTestingExperimental),
+              features.end());
+}
+
+// Check that the enabled / disabled features are used to validate the WGSL shaders.
+TEST_F(WGSLFeatureValidationTest, UsingFeatureInShaderModule) {
+    wgpu::Instance instance = CreateInstance({});
+    wgpu::Device device = CreateDeviceOnInstance(instance);
+
+    utils::CreateShaderModule(device, R"(
+        requires chromium_testing_shipped;
+    )");
+    utils::CreateShaderModule(device, R"(
+        requires chromium_testing_shipped_with_killswitch;
+    )");
+
+    ASSERT_DEVICE_ERROR(utils::CreateShaderModule(device, R"(
+        requires chromium_testing_unimplemented;
+    )"));
+    ASSERT_DEVICE_ERROR(utils::CreateShaderModule(device, R"(
+        requires chromium_testing_unsafe_experimental;
+    )"));
+    ASSERT_DEVICE_ERROR(utils::CreateShaderModule(device, R"(
+        requires chromium_testing_experimental;
+    )"));
+}
+
+}  // anonymous namespace
+}  // namespace dawn
diff --git a/src/dawn/wire/client/Instance.cpp b/src/dawn/wire/client/Instance.cpp
index f88b467..5f0c090 100644
--- a/src/dawn/wire/client/Instance.cpp
+++ b/src/dawn/wire/client/Instance.cpp
@@ -210,4 +210,14 @@
     return GetClient()->GetEventManager()->WaitAny(count, infos, timeoutNS);
 }
 
+bool Instance::HasWGSLLanguageFeature(WGPUWGSLFeatureName feature) const {
+    // TODO(dawn:2260): Implemented wgslLanguageFeatures on the wire.
+    return false;
+}
+
+size_t Instance::EnumerateWGSLLanguageFeatures(WGPUWGSLFeatureName* features) const {
+    // TODO(dawn:2260): Implemented wgslLanguageFeatures on the wire.
+    return 0;
+}
+
 }  // namespace dawn::wire::client
diff --git a/src/dawn/wire/client/Instance.h b/src/dawn/wire/client/Instance.h
index 9c9f92d..c320682 100644
--- a/src/dawn/wire/client/Instance.h
+++ b/src/dawn/wire/client/Instance.h
@@ -58,6 +58,11 @@
 
     void ProcessEvents();
     WGPUWaitStatus WaitAny(size_t count, WGPUFutureWaitInfo* infos, uint64_t timeoutNS);
+
+    bool HasWGSLLanguageFeature(WGPUWGSLFeatureName feature) const;
+    // Always writes the full list when features is not nullptr.
+    // TODO(https://github.com/webgpu-native/webgpu-headers/issues/252): Add a count argument.
+    size_t EnumerateWGSLLanguageFeatures(WGPUWGSLFeatureName* features) const;
 };
 
 }  // namespace dawn::wire::client