tint: Refactor Extensions / Enables.

* Extract ast::Enable::ExtensionKind to ast::Extension.
* Move the parsing out of ast::Enable and next to ast/extension.h
* Change the ast::Enable constructor to take the Extension, instead of
  a std::string. It's the WGSL parser's responsibility to parse, not the
  AST nodes.
* Add ProgramBuilder::Enable() helper.
* Keep ast::Module simple - keep track of the declared AST Enable nodes,
  don't do any deduplicating of the enabled extensions.
* Add the de-duplicated ast::Extensions to the sem::Module.
* Remove the kInternalExtensionForTesting enum value - we have kF16
  now, which can be used instead for testing.
* Rename kNoExtension to kNone.

Bug: tint:1472
Change-Id: I9af635e95d36991ea468e6e0bf6798bb50937edc
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/90523
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/dawn/tests/unittests/validation/ShaderModuleValidationTests.cpp b/src/dawn/tests/unittests/validation/ShaderModuleValidationTests.cpp
index 57069ed..d6add45 100644
--- a/src/dawn/tests/unittests/validation/ShaderModuleValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/ShaderModuleValidationTests.cpp
@@ -663,7 +663,7 @@
 // Test that WGSL extension used by enable directives must be allowed by WebGPU.
 TEST_F(ShaderModuleValidationTest, ExtensionMustBeAllowed) {
     ASSERT_DEVICE_ERROR(utils::CreateShaderModule(device, R"(
-enable InternalExtensionForTesting;
+enable f16;
 
 @stage(compute) @workgroup_size(1) fn main() {})"));
 }
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 0cacca4..2f4fd62 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -226,6 +226,8 @@
     "ast/enable.h",
     "ast/expression.cc",
     "ast/expression.h",
+    "ast/extension.cc",
+    "ast/extension.h",
     "ast/external_texture.cc",
     "ast/external_texture.h",
     "ast/f16.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index f4c3884..2c03e81 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -114,6 +114,8 @@
   ast/enable.h
   ast/expression.cc
   ast/expression.h
+  ast/extension.cc
+  ast/extension.h
   ast/external_texture.cc
   ast/external_texture.h
   ast/f16.cc
@@ -691,6 +693,7 @@
     ast/depth_texture_test.cc
     ast/discard_statement_test.cc
     ast/enable_test.cc
+    ast/extension_test.cc
     ast/external_texture_test.cc
     ast/f16_test.cc
     ast/f32_test.cc
diff --git a/src/tint/ast/enable.cc b/src/tint/ast/enable.cc
index 200c2be..ef43200 100644
--- a/src/tint/ast/enable.cc
+++ b/src/tint/ast/enable.cc
@@ -21,47 +21,7 @@
 
 namespace tint::ast {
 
-Enable::ExtensionKind Enable::NameToKind(const std::string& name) {
-    if (name == "chromium_experimental_dp4a") {
-        return Enable::ExtensionKind::kChromiumExperimentalDP4a;
-    }
-    if (name == "chromium_disable_uniformity_analysis") {
-        return Enable::ExtensionKind::kChromiumDisableUniformityAnalysis;
-    }
-    if (name == "f16") {
-        return Enable::ExtensionKind::kF16;
-    }
-
-    // The reserved internal extension name for testing
-    if (name == "InternalExtensionForTesting") {
-        return Enable::ExtensionKind::kInternalExtensionForTesting;
-    }
-
-    return Enable::ExtensionKind::kNoExtension;
-}
-
-std::string Enable::KindToName(ExtensionKind kind) {
-    switch (kind) {
-        case ExtensionKind::kChromiumExperimentalDP4a:
-            return "chromium_experimental_dp4a";
-        case ExtensionKind::kChromiumDisableUniformityAnalysis:
-            return "chromium_disable_uniformity_analysis";
-        case ExtensionKind::kF16:
-            return "f16";
-        // The reserved internal extension for testing
-        case ExtensionKind::kInternalExtensionForTesting:
-            return "InternalExtensionForTesting";
-        case ExtensionKind::kNoExtension:
-            // Return an empty string for kNoExtension
-            return {};
-            // No default case, as this switch must cover all ExtensionKind values.
-    }
-    // This return shall never get hit.
-    return {};
-}
-
-Enable::Enable(ProgramID pid, const Source& src, const std::string& ext_name)
-    : Base(pid, src), name(ext_name), kind(NameToKind(ext_name)) {}
+Enable::Enable(ProgramID pid, const Source& src, Extension ext) : Base(pid, src), extension(ext) {}
 
 Enable::Enable(Enable&&) = default;
 
@@ -69,6 +29,6 @@
 
 const Enable* Enable::Clone(CloneContext* ctx) const {
     auto src = ctx->Clone(source);
-    return ctx->dst->create<Enable>(src, name);
+    return ctx->dst->create<Enable>(src, extension);
 }
 }  // namespace tint::ast
diff --git a/src/tint/ast/enable.h b/src/tint/ast/enable.h
index f190b0a..674d9cb 100644
--- a/src/tint/ast/enable.h
+++ b/src/tint/ast/enable.h
@@ -16,63 +16,26 @@
 #define SRC_TINT_AST_ENABLE_H_
 
 #include <string>
-#include <unordered_set>
 #include <utility>
+#include <vector>
 
-#include "src/tint/ast/access.h"
-#include "src/tint/ast/expression.h"
+#include "src/tint/ast/extension.h"
+#include "src/tint/ast/node.h"
 
 namespace tint::ast {
 
-/// An instance of this class represents one extension mentioned in a
-/// "enable" derictive. Example:
-///       // Enable an extension named "f16"
-///       enable f16;
-class Enable : public Castable<Enable, Node> {
+/// An "enable" directive. Example:
+/// ```
+///   // Enable an extension named "f16"
+///   enable f16;
+/// ```
+class Enable final : public Castable<Enable, Node> {
   public:
-    ///  The enum class identifing each supported WGSL extension
-    enum class ExtensionKind {
-        /// An internal reserved extension for test, named
-        /// "InternalExtensionForTesting".
-        kInternalExtensionForTesting,
-        /// WGSL Extension "f16"
-        kF16,
-
-        /// An extension for the experimental feature
-        /// "chromium_experimental_dp4a".
-        /// See crbug.com/tint/1497 for more details
-        kChromiumExperimentalDP4a,
-        /// A Chromium-specific extension for disabling uniformity analysis.
-        kChromiumDisableUniformityAnalysis,
-
-        /// Reserved for representing "No extension required" or "Not a valid extension".
-        kNoExtension,
-    };
-
-    /// Convert a string of extension name into one of ExtensionKind enum value,
-    /// the result will be ExtensionKind::kNoExtension if the name is not a
-    /// known extension name. A extension node of kind kNoExtension must not
-    /// exist in the AST tree, and using a unknown extension name in WGSL code
-    /// should result in a shader-creation error.
-    /// @param name string of the extension name
-    /// @return the ExtensionKind enum value for the extension of given name, or
-    /// kNoExtension if no known extension has the given name
-    static ExtensionKind NameToKind(const std::string& name);
-
-    /// Convert the ExtensionKind enum value to corresponding extension name
-    /// string. If the given enum value is kNoExtension or don't have a known
-    /// name, return an empty string instead.
-    /// @param kind the ExtensionKind enum value
-    /// @return string of the extension name corresponding to the given kind, or
-    /// an empty string if the given enum value is kNoExtension or don't have a
-    /// known corresponding name
-    static std::string KindToName(ExtensionKind kind);
-
     /// Create a extension
     /// @param pid the identifier of the program that owns this node
     /// @param src the source of this node
-    /// @param name the name of extension
-    Enable(ProgramID pid, const Source& src, const std::string& name);
+    /// @param ext the extension
+    Enable(ProgramID pid, const Source& src, Extension ext);
     /// Move constructor
     Enable(Enable&&);
 
@@ -85,14 +48,11 @@
     const Enable* Clone(CloneContext* ctx) const override;
 
     /// The extension name
-    const std::string name;
-
-    /// The extension kind
-    const ExtensionKind kind;
+    const Extension extension;
 };
 
-///  A set of extension kinds
-using ExtensionSet = std::unordered_set<Enable::ExtensionKind>;
+/// A list of enables
+using EnableList = std::vector<const Enable*>;
 
 }  // namespace tint::ast
 
diff --git a/src/tint/ast/enable_test.cc b/src/tint/ast/enable_test.cc
index 208c85d..e8b6e5c 100644
--- a/src/tint/ast/enable_test.cc
+++ b/src/tint/ast/enable_test.cc
@@ -19,40 +19,15 @@
 namespace tint::ast {
 namespace {
 
-using AstExtensionTest = TestHelper;
+using EnableTest = TestHelper;
 
-TEST_F(AstExtensionTest, Creation) {
-    auto* ext =
-        create<Enable>(Source{Source::Range{Source::Location{20, 2}, Source::Location{20, 5}}},
-                       "InternalExtensionForTesting");
+TEST_F(EnableTest, Creation) {
+    auto* ext = create<ast::Enable>(Source{{{20, 2}, {20, 5}}}, Extension::kF16);
     EXPECT_EQ(ext->source.range.begin.line, 20u);
     EXPECT_EQ(ext->source.range.begin.column, 2u);
     EXPECT_EQ(ext->source.range.end.line, 20u);
     EXPECT_EQ(ext->source.range.end.column, 5u);
-    EXPECT_EQ(ext->kind, ast::Enable::ExtensionKind::kInternalExtensionForTesting);
-}
-
-TEST_F(AstExtensionTest, Creation_InvalidName) {
-    auto* ext = create<Enable>(
-        Source{Source::Range{Source::Location{20, 2}, Source::Location{20, 5}}}, std::string());
-    EXPECT_EQ(ext->source.range.begin.line, 20u);
-    EXPECT_EQ(ext->source.range.begin.column, 2u);
-    EXPECT_EQ(ext->source.range.end.line, 20u);
-    EXPECT_EQ(ext->source.range.end.column, 5u);
-    EXPECT_EQ(ext->kind, ast::Enable::ExtensionKind::kNoExtension);
-}
-
-TEST_F(AstExtensionTest, NameToKind_InvalidName) {
-    EXPECT_EQ(ast::Enable::NameToKind(std::string()), ast::Enable::ExtensionKind::kNoExtension);
-    EXPECT_EQ(ast::Enable::NameToKind("__ImpossibleExtensionName"),
-              ast::Enable::ExtensionKind::kNoExtension);
-    EXPECT_EQ(ast::Enable::NameToKind("123"), ast::Enable::ExtensionKind::kNoExtension);
-}
-
-TEST_F(AstExtensionTest, KindToName) {
-    EXPECT_EQ(ast::Enable::KindToName(ast::Enable::ExtensionKind::kInternalExtensionForTesting),
-              "InternalExtensionForTesting");
-    EXPECT_EQ(ast::Enable::KindToName(ast::Enable::ExtensionKind::kNoExtension), std::string());
+    EXPECT_EQ(ext->extension, Extension::kF16);
 }
 
 }  // namespace
diff --git a/src/tint/ast/extension.cc b/src/tint/ast/extension.cc
new file mode 100644
index 0000000..f03e3a0
--- /dev/null
+++ b/src/tint/ast/extension.cc
@@ -0,0 +1,51 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/extension.h"
+
+namespace tint::ast {
+
+Extension ParseExtension(const std::string& name) {
+    if (name == "chromium_experimental_dp4a") {
+        return Extension::kChromiumExperimentalDP4a;
+    }
+    if (name == "chromium_disable_uniformity_analysis") {
+        return Extension::kChromiumDisableUniformityAnalysis;
+    }
+    if (name == "f16") {
+        return Extension::kF16;
+    }
+    return Extension::kNone;
+}
+
+const char* str(Extension ext) {
+    switch (ext) {
+        case Extension::kChromiumExperimentalDP4a:
+            return "chromium_experimental_dp4a";
+        case Extension::kChromiumDisableUniformityAnalysis:
+            return "chromium_disable_uniformity_analysis";
+        case Extension::kF16:
+            return "f16";
+        case Extension::kNone:
+            return "<none>";
+    }
+    return "<unknown>";
+}
+
+std::ostream& operator<<(std::ostream& out, Extension i) {
+    out << str(i);
+    return out;
+}
+
+}  // namespace tint::ast
diff --git a/src/tint/ast/extension.h b/src/tint/ast/extension.h
new file mode 100644
index 0000000..21e9ac1
--- /dev/null
+++ b/src/tint/ast/extension.h
@@ -0,0 +1,68 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_EXTENSION_H_
+#define SRC_TINT_AST_EXTENSION_H_
+
+#include <sstream>
+#include <string>
+
+#include "src/tint/utils/unique_vector.h"
+
+namespace tint::ast {
+
+/// An enumerator of WGSL extensions
+enum class Extension {
+    /// WGSL Extension "f16"
+    kF16,
+
+    /// An extension for the experimental feature
+    /// "chromium_experimental_dp4a".
+    /// See crbug.com/tint/1497 for more details
+    kChromiumExperimentalDP4a,
+    /// A Chromium-specific extension for disabling uniformity analysis.
+    kChromiumDisableUniformityAnalysis,
+
+    /// Reserved for representing "No extension required" or "Not a valid extension".
+    kNone,
+};
+
+/// Convert a string of extension name into one of Extension enum value, the result will be
+/// Extension::kNone if the name is not a known extension name. A extension node of kind
+/// kNone must not exist in the AST tree, and using a unknown extension name in WGSL code
+/// should result in a shader-creation error.
+/// @param name string of the extension name
+/// @return the Extension enum value for the extension of given name, or kNone if no known extension
+/// has the given name
+Extension ParseExtension(const std::string& name);
+
+/// Convert the Extension enum value to corresponding extension name string.
+/// @param ext the Extension enum value
+/// @return string of the extension name corresponding to the given kind, or
+/// an empty string if the given enum value is kNone or don't have a
+/// known corresponding name
+const char* ExtensionName(Extension ext);
+
+/// @returns the name of the extension.
+const char* str(Extension i);
+
+/// Emits the name of the extension type.
+std::ostream& operator<<(std::ostream& out, Extension i);
+
+// A unique vector of extensions
+using Extensions = utils::UniqueVector<Extension>;
+
+}  // namespace tint::ast
+
+#endif  // SRC_TINT_AST_EXTENSION_H_
diff --git a/src/tint/ast/extension_test.cc b/src/tint/ast/extension_test.cc
new file mode 100644
index 0000000..ed27674b
--- /dev/null
+++ b/src/tint/ast/extension_test.cc
@@ -0,0 +1,36 @@
+
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/extension.h"
+
+#include "gtest/gtest.h"
+
+namespace tint::ast {
+namespace {
+
+TEST(ExtensionTest, NameToKind_InvalidName) {
+    EXPECT_EQ(ParseExtension("f16"), Extension::kF16);
+    EXPECT_EQ(ParseExtension(""), Extension::kNone);
+    EXPECT_EQ(ParseExtension("__ImpossibleExtensionName"), Extension::kNone);
+    EXPECT_EQ(ParseExtension("123"), Extension::kNone);
+}
+
+TEST(ExtensionTest, KindToName) {
+    EXPECT_EQ(std::string(str(Extension::kF16)), "f16");
+    EXPECT_EQ(std::string(str(Extension::kNone)), "<none>");
+}
+
+}  // namespace
+}  // namespace tint::ast
diff --git a/src/tint/ast/module.cc b/src/tint/ast/module.cc
index e163c19..40dff98 100644
--- a/src/tint/ast/module.cc
+++ b/src/tint/ast/module.cc
@@ -68,18 +68,18 @@
             TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, var, program_id);
             global_variables_.push_back(var);
         },
-        [&](const Enable* ext) {
-            TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, ext, program_id);
-            extensions_.insert(ext->kind);
+        [&](const Enable* enable) {
+            TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, enable, program_id);
+            enables_.push_back(enable);
         },
         [&](Default) { TINT_ICE(AST, diags) << "Unknown global declaration type"; });
 }
 
-void Module::AddEnable(const ast::Enable* ext) {
-    TINT_ASSERT(AST, ext);
-    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, ext, program_id);
-    global_declarations_.push_back(ext);
-    extensions_.insert(ext->kind);
+void Module::AddEnable(const ast::Enable* enable) {
+    TINT_ASSERT(AST, enable);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, enable, program_id);
+    global_declarations_.push_back(enable);
+    enables_.push_back(enable);
 }
 
 void Module::AddGlobalVariable(const ast::Variable* var) {
@@ -117,7 +117,7 @@
     type_decls_.clear();
     functions_.clear();
     global_variables_.clear();
-    extensions_.clear();
+    enables_.clear();
 
     for (auto* decl : global_declarations_) {
         if (!decl) {
diff --git a/src/tint/ast/module.h b/src/tint/ast/module.h
index d8be2ed..45b1ec6 100644
--- a/src/tint/ast/module.h
+++ b/src/tint/ast/module.h
@@ -78,7 +78,7 @@
     VariableList& GlobalVariables() { return global_variables_; }
 
     /// @returns the extension set for the module
-    const ExtensionSet& Extensions() const { return extensions_; }
+    const EnableList& Enables() const { return enables_; }
 
     /// Adds a type declaration to the Builder.
     /// @param decl the type declaration to add
@@ -120,7 +120,7 @@
     std::vector<const TypeDecl*> type_decls_;
     FunctionList functions_;
     VariableList global_variables_;
-    ExtensionSet extensions_;
+    EnableList enables_;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/inspector/inspector.cc b/src/tint/inspector/inspector.cc
index 2bfe815..9a2afb8 100644
--- a/src/tint/inspector/inspector.cc
+++ b/src/tint/inspector/inspector.cc
@@ -19,6 +19,7 @@
 
 #include "src/tint/ast/bool_literal_expression.h"
 #include "src/tint/ast/call_expression.h"
+#include "src/tint/ast/extension.h"
 #include "src/tint/ast/float_literal_expression.h"
 #include "src/tint/ast/id_attribute.h"
 #include "src/tint/ast/interpolate_attribute.h"
@@ -32,6 +33,7 @@
 #include "src/tint/sem/function.h"
 #include "src/tint/sem/i32.h"
 #include "src/tint/sem/matrix.h"
+#include "src/tint/sem/module.h"
 #include "src/tint/sem/multisampled_texture.h"
 #include "src/tint/sem/sampled_texture.h"
 #include "src/tint/sem/statement.h"
@@ -544,16 +546,13 @@
 }
 
 std::vector<std::string> Inspector::GetUsedExtensionNames() {
-    std::vector<std::string> result;
-
-    ast::ExtensionSet set = program_->AST().Extensions();
-    result.reserve(set.size());
-    for (auto kind : set) {
-        std::string name = ast::Enable::KindToName(kind);
-        result.push_back(name);
+    auto& extensions = program_->Sem().Module()->Extensions();
+    std::vector<std::string> out;
+    out.reserve(extensions.size());
+    for (auto ext : extensions) {
+        out.push_back(ast::str(ext));
     }
-
-    return result;
+    return out;
 }
 
 std::vector<std::pair<std::string, Source>> Inspector::GetEnableDirectives() {
@@ -563,7 +562,7 @@
     auto global_decls = program_->AST().GlobalDeclarations();
     for (auto* node : global_decls) {
         if (auto* ext = node->As<ast::Enable>()) {
-            result.push_back({ext->name, ext->source});
+            result.push_back({ast::str(ext->extension), ext->source});
         }
     }
 
diff --git a/src/tint/inspector/inspector_test.cc b/src/tint/inspector/inspector_test.cc
index 18a33b1..033f5b8 100644
--- a/src/tint/inspector/inspector_test.cc
+++ b/src/tint/inspector/inspector_test.cc
@@ -2849,7 +2849,7 @@
 // Test calling GetUsedExtensionNames on a shader with valid extension.
 TEST_F(InspectorGetUsedExtensionNamesTest, Simple) {
     std::string shader = R"(
-enable InternalExtensionForTesting;
+enable f16;
 
 @stage(fragment)
 fn main() {
@@ -2859,15 +2859,15 @@
 
     auto result = inspector.GetUsedExtensionNames();
     EXPECT_EQ(result.size(), 1u);
-    EXPECT_EQ(result[0], "InternalExtensionForTesting");
+    EXPECT_EQ(result[0], "f16");
 }
 
 // Test calling GetUsedExtensionNames on a shader with a extension enabled for
 // multiple times.
 TEST_F(InspectorGetUsedExtensionNamesTest, Duplicated) {
     std::string shader = R"(
-enable InternalExtensionForTesting;
-enable InternalExtensionForTesting;
+enable f16;
+enable f16;
 
 @stage(fragment)
 fn main() {
@@ -2877,7 +2877,7 @@
 
     auto result = inspector.GetUsedExtensionNames();
     EXPECT_EQ(result.size(), 1u);
-    EXPECT_EQ(result[0], "InternalExtensionForTesting");
+    EXPECT_EQ(result[0], "f16");
 }
 
 // Test calling GetEnableDirectives on a empty shader.
@@ -2906,7 +2906,7 @@
 // Test calling GetEnableDirectives on a shader with valid extension.
 TEST_F(InspectorGetEnableDirectivesTest, Simple) {
     std::string shader = R"(
-enable InternalExtensionForTesting;
+enable f16;
 
 @stage(fragment)
 fn main() {
@@ -2916,17 +2916,17 @@
 
     auto result = inspector.GetEnableDirectives();
     EXPECT_EQ(result.size(), 1u);
-    EXPECT_EQ(result[0].first, "InternalExtensionForTesting");
-    EXPECT_EQ(result[0].second.range, (Source::Range{{2, 8}, {2, 35}}));
+    EXPECT_EQ(result[0].first, "f16");
+    EXPECT_EQ(result[0].second.range, (Source::Range{{2, 8}, {2, 11}}));
 }
 
 // Test calling GetEnableDirectives on a shader with a extension enabled for
 // multiple times.
 TEST_F(InspectorGetEnableDirectivesTest, Duplicated) {
     std::string shader = R"(
-enable InternalExtensionForTesting;
+enable f16;
 
-enable InternalExtensionForTesting;
+enable f16;
 @stage(fragment)
 fn main() {
 })";
@@ -2935,10 +2935,10 @@
 
     auto result = inspector.GetEnableDirectives();
     EXPECT_EQ(result.size(), 2u);
-    EXPECT_EQ(result[0].first, "InternalExtensionForTesting");
-    EXPECT_EQ(result[0].second.range, (Source::Range{{2, 8}, {2, 35}}));
-    EXPECT_EQ(result[1].first, "InternalExtensionForTesting");
-    EXPECT_EQ(result[1].second.range, (Source::Range{{4, 8}, {4, 35}}));
+    EXPECT_EQ(result[0].first, "f16");
+    EXPECT_EQ(result[0].second.range, (Source::Range{{2, 8}, {2, 11}}));
+    EXPECT_EQ(result[1].first, "f16");
+    EXPECT_EQ(result[1].second.range, (Source::Range{{4, 8}, {4, 11}}));
 }
 
 // Crash was occuring in ::GenerateSamplerTargets, when
diff --git a/src/tint/program_builder.h b/src/tint/program_builder.h
index a91d87c..32c0e93 100644
--- a/src/tint/program_builder.h
+++ b/src/tint/program_builder.h
@@ -39,6 +39,7 @@
 #include "src/tint/ast/disable_validation_attribute.h"
 #include "src/tint/ast/discard_statement.h"
 #include "src/tint/ast/enable.h"
+#include "src/tint/ast/extension.h"
 #include "src/tint/ast/external_texture.h"
 #include "src/tint/ast/f16.h"
 #include "src/tint/ast/f32.h"
@@ -1307,6 +1308,15 @@
         return Construct(ty.array(subtype, std::forward<EXPR>(n)), std::forward<ARGS>(args)...);
     }
 
+    /// Adds the extension to the list of enable directives at the top of the module.
+    /// @param ext the extension to enable
+    /// @return an `ast::Enable` enabling the given extension.
+    const ast::Enable* Enable(ast::Extension ext) {
+        auto* enable = create<ast::Enable>(ext);
+        AST().AddEnable(enable);
+        return enable;
+    }
+
     /// @param name the variable name
     /// @param type the variable type
     /// @param optional the optional variable settings.
diff --git a/src/tint/reader/wgsl/parser_impl.cc b/src/tint/reader/wgsl/parser_impl.cc
index abd97e5..4f0b152 100644
--- a/src/tint/reader/wgsl/parser_impl.cc
+++ b/src/tint/reader/wgsl/parser_impl.cc
@@ -366,13 +366,11 @@
             return Failure::kErrored;
         }
 
-        if (ast::Enable::NameToKind(name.value) != ast::Enable::ExtensionKind::kNoExtension) {
-            const ast::Enable* extension = create<ast::Enable>(name.source, name.value);
-            builder_.AST().AddEnable(extension);
-        } else {
-            // Error if an unknown extension is used
+        auto extension = ast::ParseExtension(name.value);
+        if (extension == ast::Extension::kNone) {
             return add_error(name.source, "unsupported extension: '" + name.value + "'");
         }
+        builder_.AST().AddEnable(create<ast::Enable>(name.source, extension));
 
         return true;
     });
diff --git a/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc b/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
index 0fd6b80..bdf7eeb 100644
--- a/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
@@ -23,41 +23,36 @@
 
 // Test a valid enable directive.
 TEST_F(EnableDirectiveTest, Valid) {
-    auto p = parser("enable InternalExtensionForTesting;");
+    auto p = parser("enable f16;");
     p->enable_directive();
     EXPECT_FALSE(p->has_error()) << p->error();
     auto program = p->program();
     auto& ast = program.AST();
-    EXPECT_EQ(ast.Extensions(),
-              ast::ExtensionSet{ast::Enable::ExtensionKind::kInternalExtensionForTesting});
-    EXPECT_EQ(ast.GlobalDeclarations().size(), 1u);
-    auto* node = ast.GlobalDeclarations()[0]->As<ast::Enable>();
-    EXPECT_TRUE(node != nullptr);
-    EXPECT_EQ(node->name, "InternalExtensionForTesting");
-    EXPECT_EQ(node->kind, ast::Enable::ExtensionKind::kInternalExtensionForTesting);
+    ASSERT_EQ(ast.Enables().size(), 1u);
+    auto* enable = ast.Enables()[0];
+    EXPECT_EQ(enable->extension, ast::Extension::kF16);
+    ASSERT_EQ(ast.GlobalDeclarations().size(), 1u);
+    EXPECT_EQ(ast.GlobalDeclarations()[0], enable);
 }
 
 // Test multiple enable directives for a same extension.
 TEST_F(EnableDirectiveTest, EnableMultipleTime) {
     auto p = parser(R"(
-enable InternalExtensionForTesting;
-enable InternalExtensionForTesting;
+enable f16;
+enable f16;
 )");
     p->translation_unit();
     EXPECT_FALSE(p->has_error()) << p->error();
     auto program = p->program();
     auto& ast = program.AST();
-    EXPECT_EQ(ast.Extensions(),
-              ast::ExtensionSet{ast::Enable::ExtensionKind::kInternalExtensionForTesting});
-    EXPECT_EQ(ast.GlobalDeclarations().size(), 2u);
-    auto* node1 = ast.GlobalDeclarations()[0]->As<ast::Enable>();
-    EXPECT_TRUE(node1 != nullptr);
-    EXPECT_EQ(node1->name, "InternalExtensionForTesting");
-    EXPECT_EQ(node1->kind, ast::Enable::ExtensionKind::kInternalExtensionForTesting);
-    auto* node2 = ast.GlobalDeclarations()[1]->As<ast::Enable>();
-    EXPECT_TRUE(node2 != nullptr);
-    EXPECT_EQ(node2->name, "InternalExtensionForTesting");
-    EXPECT_EQ(node2->kind, ast::Enable::ExtensionKind::kInternalExtensionForTesting);
+    ASSERT_EQ(ast.Enables().size(), 2u);
+    auto* enable_a = ast.Enables()[0];
+    auto* enable_b = ast.Enables()[1];
+    EXPECT_EQ(enable_a->extension, ast::Extension::kF16);
+    EXPECT_EQ(enable_b->extension, ast::Extension::kF16);
+    ASSERT_EQ(ast.GlobalDeclarations().size(), 2u);
+    EXPECT_EQ(ast.GlobalDeclarations()[0], enable_a);
+    EXPECT_EQ(ast.GlobalDeclarations()[1], enable_b);
 }
 
 // Test an unknown extension identifier.
@@ -69,42 +64,42 @@
     EXPECT_EQ(p->error(), "1:8: unsupported extension: 'NotAValidExtensionName'");
     auto program = p->program();
     auto& ast = program.AST();
-    EXPECT_EQ(ast.Extensions().size(), 0u);
+    EXPECT_EQ(ast.Enables().size(), 0u);
     EXPECT_EQ(ast.GlobalDeclarations().size(), 0u);
 }
 
-// Test an enable directive missing ending semiclon.
-TEST_F(EnableDirectiveTest, MissingEndingSemiclon) {
-    auto p = parser("enable InternalExtensionForTesting");
+// Test an enable directive missing ending semicolon.
+TEST_F(EnableDirectiveTest, MissingEndingSemicolon) {
+    auto p = parser("enable f16");
     p->translation_unit();
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:35: expected ';' for enable directive");
+    EXPECT_EQ(p->error(), "1:11: expected ';' for enable directive");
     auto program = p->program();
     auto& ast = program.AST();
-    EXPECT_EQ(ast.Extensions().size(), 0u);
+    EXPECT_EQ(ast.Enables().size(), 0u);
     EXPECT_EQ(ast.GlobalDeclarations().size(), 0u);
 }
 
 // Test using invalid tokens in an enable directive.
 TEST_F(EnableDirectiveTest, InvalidTokens) {
     {
-        auto p = parser("enable InternalExtensionForTesting<;");
+        auto p = parser("enable f16<;");
         p->translation_unit();
         EXPECT_TRUE(p->has_error());
-        EXPECT_EQ(p->error(), "1:35: expected ';' for enable directive");
+        EXPECT_EQ(p->error(), "1:11: expected ';' for enable directive");
         auto program = p->program();
         auto& ast = program.AST();
-        EXPECT_EQ(ast.Extensions().size(), 0u);
+        EXPECT_EQ(ast.Enables().size(), 0u);
         EXPECT_EQ(ast.GlobalDeclarations().size(), 0u);
     }
     {
-        auto p = parser("enable <InternalExtensionForTesting;");
+        auto p = parser("enable <f16;");
         p->translation_unit();
         EXPECT_TRUE(p->has_error());
         EXPECT_EQ(p->error(), "1:8: invalid extension name");
         auto program = p->program();
         auto& ast = program.AST();
-        EXPECT_EQ(ast.Extensions().size(), 0u);
+        EXPECT_EQ(ast.Enables().size(), 0u);
         EXPECT_EQ(ast.GlobalDeclarations().size(), 0u);
     }
     {
@@ -114,7 +109,7 @@
         EXPECT_EQ(p->error(), "1:8: invalid extension name");
         auto program = p->program();
         auto& ast = program.AST();
-        EXPECT_EQ(ast.Extensions().size(), 0u);
+        EXPECT_EQ(ast.Enables().size(), 0u);
         EXPECT_EQ(ast.GlobalDeclarations().size(), 0u);
     }
     {
@@ -124,7 +119,7 @@
         EXPECT_EQ(p->error(), "1:8: invalid extension name");
         auto program = p->program();
         auto& ast = program.AST();
-        EXPECT_EQ(ast.Extensions().size(), 0u);
+        EXPECT_EQ(ast.Enables().size(), 0u);
         EXPECT_EQ(ast.GlobalDeclarations().size(), 0u);
     }
 }
@@ -133,35 +128,39 @@
 TEST_F(EnableDirectiveTest, FollowingOtherGlobalDecl) {
     auto p = parser(R"(
 var<private> t: f32 = 0f;
-enable InternalExtensionForTesting;
+enable f16;
 )");
     p->translation_unit();
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), "3:1: enable directives must come before all global declarations");
     auto program = p->program();
     auto& ast = program.AST();
-    // Accept the enable directive although it cause an error
-    EXPECT_EQ(ast.Extensions(),
-              ast::ExtensionSet{ast::Enable::ExtensionKind::kInternalExtensionForTesting});
-    EXPECT_EQ(ast.GlobalDeclarations().size(), 2u);
+    // Accept the enable directive although it caused an error
+    ASSERT_EQ(ast.Enables().size(), 1u);
+    auto* enable = ast.Enables()[0];
+    EXPECT_EQ(enable->extension, ast::Extension::kF16);
+    ASSERT_EQ(ast.GlobalDeclarations().size(), 2u);
+    EXPECT_EQ(ast.GlobalDeclarations()[1], enable);
 }
 
-// Test an enable directive go after an empty semiclon.
-TEST_F(EnableDirectiveTest, FollowingEmptySemiclon) {
+// Test an enable directive go after an empty semicolon.
+TEST_F(EnableDirectiveTest, FollowingEmptySemicolon) {
     auto p = parser(R"(
 ;
-enable InternalExtensionForTesting;
+enable f16;
 )");
     p->translation_unit();
-    // An empty semiclon is treated as a global declaration
+    // An empty semicolon is treated as a global declaration
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), "3:1: enable directives must come before all global declarations");
     auto program = p->program();
     auto& ast = program.AST();
     // Accept the enable directive although it cause an error
-    EXPECT_EQ(ast.Extensions(),
-              ast::ExtensionSet{ast::Enable::ExtensionKind::kInternalExtensionForTesting});
-    EXPECT_EQ(ast.GlobalDeclarations().size(), 1u);
+    ASSERT_EQ(ast.Enables().size(), 1u);
+    auto* enable = ast.Enables()[0];
+    EXPECT_EQ(enable->extension, ast::Extension::kF16);
+    ASSERT_EQ(ast.GlobalDeclarations().size(), 1u);
+    EXPECT_EQ(ast.GlobalDeclarations()[0], enable);
 }
 
 }  // namespace
diff --git a/src/tint/resolver/builtin_validation_test.cc b/src/tint/resolver/builtin_validation_test.cc
index 0e48b55..770d8d0 100644
--- a/src/tint/resolver/builtin_validation_test.cc
+++ b/src/tint/resolver/builtin_validation_test.cc
@@ -378,10 +378,7 @@
 TEST_F(ResolverDP4aExtensionValidationTest, Dot4I8PackedWithExtension) {
     // enable chromium_experimental_dp4a;
     // fn func { return dot4I8Packed(1u, 2u); }
-    auto* ext =
-        create<ast::Enable>(Source{Source::Range{Source::Location{10, 2}, Source::Location{10, 5}}},
-                            "chromium_experimental_dp4a");
-    AST().AddEnable(ext);
+    Enable(ast::Extension::kChromiumExperimentalDP4a);
 
     Func("func", {}, ty.i32(),
          {
@@ -409,10 +406,7 @@
 TEST_F(ResolverDP4aExtensionValidationTest, Dot4U8PackedWithExtension) {
     // enable chromium_experimental_dp4a;
     // fn func { return dot4U8Packed(1u, 2u); }
-    auto* ext =
-        create<ast::Enable>(Source{Source::Range{Source::Location{10, 2}, Source::Location{10, 5}}},
-                            "chromium_experimental_dp4a");
-    AST().AddEnable(ext);
+    Enable(ast::Extension::kChromiumExperimentalDP4a);
 
     Func("func", {}, ty.u32(),
          {
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 4993cfe..b9c0833 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -101,9 +101,6 @@
         return false;
     }
 
-    // Create the semantic module
-    builder_->Sem().SetModule(builder_->create<sem::Module>(dependencies_.ordered_globals));
-
     bool result = ResolveInternal();
 
     if (!result && !diagnostics_.contains_errors()) {
@@ -111,6 +108,10 @@
         return false;
     }
 
+    // Create the semantic module
+    builder_->Sem().SetModule(builder_->create<sem::Module>(
+        std::move(dependencies_.ordered_globals), std::move(enabled_extensions_)));
+
     return result;
 }
 
@@ -120,19 +121,16 @@
     // Process all module-scope declarations in dependency order.
     for (auto* decl : dependencies_.ordered_globals) {
         Mark(decl);
-        // Enable directives don't have sem node.
-        if (decl->Is<ast::Enable>()) {
-            continue;
-        }
-        if (!Switch(
+        if (!Switch<bool>(
                 decl,  //
+                [&](const ast::Enable* e) { return Enable(e); },
                 [&](const ast::TypeDecl* td) { return TypeDecl(td); },
                 [&](const ast::Function* func) { return Function(func); },
                 [&](const ast::Variable* var) { return GlobalVariable(var); },
                 [&](Default) {
                     TINT_UNREACHABLE(Resolver, diagnostics_)
                         << "unhandled global declaration: " << decl->TypeInfo().name;
-                    return nullptr;
+                    return false;
                 })) {
             return false;
         }
@@ -146,8 +144,10 @@
         return false;
     }
 
-    if (!AnalyzeUniformity(builder_, dependencies_)) {
-        // TODO(jrprice): Reject programs that fail uniformity analysis.
+    if (!enabled_extensions_.contains(ast::Extension::kChromiumDisableUniformityAnalysis)) {
+        if (!AnalyzeUniformity(builder_, dependencies_)) {
+            // TODO(jrprice): Reject programs that fail uniformity analysis.
+        }
     }
 
     bool result = true;
@@ -174,7 +174,7 @@
         [&](const ast::U32*) { return builder_->create<sem::U32>(); },
         [&](const ast::F16* t) -> sem::F16* {
             // Validate if f16 type is allowed.
-            if (builder_->AST().Extensions().count(ast::Enable::ExtensionKind::kF16) == 0) {
+            if (!enabled_extensions_.contains(ast::Extension::kF16)) {
                 AddError("f16 used without 'f16' extension enabled", t->source);
                 return nullptr;
             }
@@ -1358,7 +1358,7 @@
 
     current_function_->AddDirectlyCalledBuiltin(builtin);
 
-    if (!validator_.RequiredExtensionForBuiltinFunction(call, builder_->AST().Extensions())) {
+    if (!validator_.RequiredExtensionForBuiltinFunction(call, enabled_extensions_)) {
         return nullptr;
     }
 
@@ -1750,6 +1750,11 @@
     return sem;
 }
 
+bool Resolver::Enable(const ast::Enable* enable) {
+    enabled_extensions_.add(enable->extension);
+    return true;
+}
+
 sem::Type* Resolver::TypeDecl(const ast::TypeDecl* named_type) {
     sem::Type* result = nullptr;
     if (auto* alias = named_type->As<ast::Alias>()) {
diff --git a/src/tint/resolver/resolver.h b/src/tint/resolver/resolver.h
index 348c8e7..865c243 100644
--- a/src/tint/resolver/resolver.h
+++ b/src/tint/resolver/resolver.h
@@ -228,6 +228,10 @@
     /// @param ty the ast::Type
     sem::Type* Type(const ast::Type* ty);
 
+    /// @param enable the enable declaration
+    /// @returns the resolved extension
+    bool Enable(const ast::Enable* enable);
+
     /// @param named_type the named type to resolve
     /// @returns the resolved semantic type
     sem::Type* TypeDecl(const ast::TypeDecl* named_type);
@@ -351,6 +355,7 @@
     DependencyGraph dependencies_;
     SemHelper sem_;
     Validator validator_;
+    ast::Extensions enabled_extensions_;
     std::vector<sem::Function*> entry_points_;
     std::unordered_map<const sem::Type*, const Source&> atomic_composite_info_;
     std::unordered_set<const ast::Node*> marked_;
diff --git a/src/tint/resolver/type_validation_test.cc b/src/tint/resolver/type_validation_test.cc
index 5f4617e..26b705f 100644
--- a/src/tint/resolver/type_validation_test.cc
+++ b/src/tint/resolver/type_validation_test.cc
@@ -665,8 +665,8 @@
 TEST_F(ResolverTypeValidationTest, F16TypeUsedWithExtension) {
     // enable f16;
     // var<private> v : f16;
-    auto* ext = create<ast::Enable>("f16");
-    AST().AddEnable(ext);
+    Enable(ast::Extension::kF16);
+
     Global("v", ty.f16(), ast::StorageClass::kPrivate);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
diff --git a/src/tint/resolver/uniformity.cc b/src/tint/resolver/uniformity.cc
index 350e93b..273a07e 100644
--- a/src/tint/resolver/uniformity.cc
+++ b/src/tint/resolver/uniformity.cc
@@ -1548,11 +1548,6 @@
 }  // namespace
 
 bool AnalyzeUniformity(ProgramBuilder* builder, const DependencyGraph& dependency_graph) {
-    if (builder->AST().Extensions().count(
-            ast::Enable::ExtensionKind::kChromiumDisableUniformityAnalysis)) {
-        return true;
-    }
-
     UniformityGraph graph(builder);
     return graph.Build(dependency_graph);
 }
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index b278954..9f698e6 100644
--- a/src/tint/resolver/validator.cc
+++ b/src/tint/resolver/validator.cc
@@ -1553,21 +1553,22 @@
            check_arg_is_constexpr(sem::ParameterUsage::kComponent, 0, 3);
 }
 
-bool Validator::RequiredExtensionForBuiltinFunction(const sem::Call* call,
-                                                    const ast::ExtensionSet& extensionSet) const {
+bool Validator::RequiredExtensionForBuiltinFunction(
+    const sem::Call* call,
+    const ast::Extensions& enabled_extensions) const {
     const auto* builtin = call->Target()->As<sem::Builtin>();
     if (!builtin) {
         return true;
     }
 
     const auto extension = builtin->RequiredExtension();
-    if (extension == ast::Enable::ExtensionKind::kNoExtension) {
+    if (extension == ast::Extension::kNone) {
         return true;
     }
 
-    if (extensionSet.find(extension) == extensionSet.cend()) {
+    if (!enabled_extensions.contains(extension)) {
         AddError("cannot call built-in function '" + std::string(builtin->str()) +
-                     "' without extension " + ast::Enable::KindToName(extension),
+                     "' without extension " + ast::str(extension),
                  call->Declaration()->source);
         return false;
     }
diff --git a/src/tint/resolver/validator.h b/src/tint/resolver/validator.h
index 6efb543..a8c18d5 100644
--- a/src/tint/resolver/validator.h
+++ b/src/tint/resolver/validator.h
@@ -361,10 +361,10 @@
 
     /// Validates an optional builtin function and its required extension.
     /// @param call the builtin call to validate
-    /// @param extensionSet all the extensions declared in current module
+    /// @param enabled_extensions all the extensions declared in current module
     /// @returns true on success, false otherwise
     bool RequiredExtensionForBuiltinFunction(const sem::Call* call,
-                                             const ast::ExtensionSet& extensionSet) const;
+                                             const ast::Extensions& enabled_extensions) const;
 
     /// Validates there are no duplicate attributes
     /// @param attributes the list of attributes to validate
diff --git a/src/tint/sem/builtin.cc b/src/tint/sem/builtin.cc
index faf451e..bb2878b 100644
--- a/src/tint/sem/builtin.cc
+++ b/src/tint/sem/builtin.cc
@@ -153,11 +153,11 @@
     return false;
 }
 
-ast::Enable::ExtensionKind Builtin::RequiredExtension() const {
+ast::Extension Builtin::RequiredExtension() const {
     if (IsDP4a()) {
-        return ast::Enable::ExtensionKind::kChromiumExperimentalDP4a;
+        return ast::Extension::kChromiumExperimentalDP4a;
     }
-    return ast::Enable::ExtensionKind::kNoExtension;
+    return ast::Extension::kNone;
 }
 
 }  // namespace tint::sem
diff --git a/src/tint/sem/builtin.h b/src/tint/sem/builtin.h
index 4752f16..1dc61ad 100644
--- a/src/tint/sem/builtin.h
+++ b/src/tint/sem/builtin.h
@@ -18,6 +18,7 @@
 #include <string>
 #include <vector>
 
+#include "src/tint/ast/extension.h"
 #include "src/tint/sem/builtin_type.h"
 #include "src/tint/sem/call_target.h"
 #include "src/tint/sem/pipeline_stage_set.h"
@@ -144,8 +145,8 @@
     bool HasSideEffects() const;
 
     /// @returns the required extension of this builtin function. Returns
-    /// ast::Enable::ExtensionKind::kNoExtension if no extension is required.
-    ast::Enable::ExtensionKind RequiredExtension() const;
+    /// ast::Extension::kNone if no extension is required.
+    ast::Extension RequiredExtension() const;
 
   private:
     const BuiltinType type_;
diff --git a/src/tint/sem/module.cc b/src/tint/sem/module.cc
index 83b7136..7c60650 100644
--- a/src/tint/sem/module.cc
+++ b/src/tint/sem/module.cc
@@ -21,8 +21,8 @@
 
 namespace tint::sem {
 
-Module::Module(std::vector<const ast::Node*> dep_ordered_decls)
-    : dep_ordered_decls_(std::move(dep_ordered_decls)) {}
+Module::Module(std::vector<const ast::Node*> dep_ordered_decls, ast::Extensions extensions)
+    : dep_ordered_decls_(std::move(dep_ordered_decls)), extensions_(std::move(extensions)) {}
 
 Module::~Module() = default;
 
diff --git a/src/tint/sem/module.h b/src/tint/sem/module.h
index c265d4e..a7b3d45 100644
--- a/src/tint/sem/module.h
+++ b/src/tint/sem/module.h
@@ -17,6 +17,7 @@
 
 #include <vector>
 
+#include "src/tint/ast/extension.h"
 #include "src/tint/sem/node.h"
 
 // Forward declarations
@@ -33,7 +34,8 @@
   public:
     /// Constructor
     /// @param dep_ordered_decls the dependency-ordered module-scope declarations
-    explicit Module(std::vector<const ast::Node*> dep_ordered_decls);
+    /// @param extensions the list of enabled extensions in the module
+    Module(std::vector<const ast::Node*> dep_ordered_decls, ast::Extensions extensions);
 
     /// Destructor
     ~Module() override;
@@ -43,8 +45,12 @@
         return dep_ordered_decls_;
     }
 
+    /// @returns the list of enabled extensions in the module
+    const ast::Extensions& Extensions() const { return extensions_; }
+
   private:
     const std::vector<const ast::Node*> dep_ordered_decls_;
+    ast::Extensions extensions_;
 };
 
 }  // namespace tint::sem
diff --git a/src/tint/transform/disable_uniformity_analysis.cc b/src/tint/transform/disable_uniformity_analysis.cc
index c025031..7a30023 100644
--- a/src/tint/transform/disable_uniformity_analysis.cc
+++ b/src/tint/transform/disable_uniformity_analysis.cc
@@ -17,6 +17,7 @@
 #include <utility>
 
 #include "src/tint/program_builder.h"
+#include "src/tint/sem/module.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::transform::DisableUniformityAnalysis);
 
@@ -27,13 +28,12 @@
 DisableUniformityAnalysis::~DisableUniformityAnalysis() = default;
 
 bool DisableUniformityAnalysis::ShouldRun(const Program* program, const DataMap&) const {
-    return !program->AST().Extensions().count(
-        ast::Enable::ExtensionKind::kChromiumDisableUniformityAnalysis);
+    return !program->Sem().Module()->Extensions().contains(
+        ast::Extension::kChromiumDisableUniformityAnalysis);
 }
 
 void DisableUniformityAnalysis::Run(CloneContext& ctx, const DataMap&, DataMap&) const {
-    ctx.dst->AST().AddEnable(ctx.dst->create<ast::Enable>(
-        ast::Enable::KindToName(ast::Enable::ExtensionKind::kChromiumDisableUniformityAnalysis)));
+    ctx.dst->Enable(ast::Extension::kChromiumDisableUniformityAnalysis);
     ctx.Clone();
 }
 
diff --git a/src/tint/writer/hlsl/generator_impl_builtin_test.cc b/src/tint/writer/hlsl/generator_impl_builtin_test.cc
index 6c573ff..f6e0d98 100644
--- a/src/tint/writer/hlsl/generator_impl_builtin_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_builtin_test.cc
@@ -727,10 +727,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Builtin, Dot4I8Packed) {
-    auto* ext =
-        create<ast::Enable>(Source{Source::Range{Source::Location{10, 2}, Source::Location{10, 5}}},
-                            "chromium_experimental_dp4a");
-    AST().AddEnable(ext);
+    Enable(ast::Extension::kChromiumExperimentalDP4a);
 
     auto* val1 = Var("val1", ty.u32());
     auto* val2 = Var("val2", ty.u32());
@@ -756,10 +753,7 @@
 }
 
 TEST_F(HlslGeneratorImplTest_Builtin, Dot4U8Packed) {
-    auto* ext =
-        create<ast::Enable>(Source{Source::Range{Source::Location{10, 2}, Source::Location{10, 5}}},
-                            "chromium_experimental_dp4a");
-    AST().AddEnable(ext);
+    Enable(ast::Extension::kChromiumExperimentalDP4a);
 
     auto* val1 = Var("val1", ty.u32());
     auto* val2 = Var("val2", ty.u32());
diff --git a/src/tint/writer/spirv/builder.cc b/src/tint/writer/spirv/builder.cc
index aaa64ce..0b3118a 100644
--- a/src/tint/writer/spirv/builder.cc
+++ b/src/tint/writer/spirv/builder.cc
@@ -256,7 +256,7 @@
     push_memory_model(spv::Op::OpMemoryModel,
                       {U32Operand(SpvAddressingModelLogical), U32Operand(SpvMemoryModelGLSL450)});
 
-    for (auto ext : builder_.AST().Extensions()) {
+    for (auto ext : builder_.Sem().Module()->Extensions()) {
         GenerateExtension(ext);
     }
 
@@ -366,7 +366,7 @@
     }
 }
 
-bool Builder::GenerateExtension(ast::Enable::ExtensionKind) {
+bool Builder::GenerateExtension(ast::Extension) {
     /*
     For each supported extension, push corresponding capability into the builder.
     For example:
diff --git a/src/tint/writer/spirv/builder.h b/src/tint/writer/spirv/builder.h
index 34cfd76..1745ed5 100644
--- a/src/tint/writer/spirv/builder.h
+++ b/src/tint/writer/spirv/builder.h
@@ -224,11 +224,11 @@
                                      ast::InterpolationType type,
                                      ast::InterpolationSampling sampling);
 
-    /// Generates a extension for the given extension kind. Emits an error and
-    /// returns false if the extension kind is not supported.
-    /// @param kind ExtensionKind of the extension to generate
+    /// Generates the enabling of an extension. Emits an error and returns false if the extension is
+    /// not supported.
+    /// @param ext the extension to generate
     /// @returns true on success.
-    bool GenerateExtension(ast::Enable::ExtensionKind kind);
+    bool GenerateExtension(ast::Extension ext);
     /// Generates a label for the given id. Emits an error and returns false if
     /// we're currently outside a function.
     /// @param id the id to use for the label
diff --git a/src/tint/writer/wgsl/generator_impl.cc b/src/tint/writer/wgsl/generator_impl.cc
index 121d904..677421d 100644
--- a/src/tint/writer/wgsl/generator_impl.cc
+++ b/src/tint/writer/wgsl/generator_impl.cc
@@ -62,12 +62,12 @@
 
 bool GeneratorImpl::Generate() {
     // Generate enable directives before any other global declarations.
-    for (auto ext : program_->AST().Extensions()) {
-        if (!EmitEnableDirective(ext)) {
+    for (auto enable : program_->AST().Enables()) {
+        if (!EmitEnable(enable)) {
             return false;
         }
     }
-    if (!program_->AST().Extensions().empty()) {
+    if (!program_->AST().Enables().empty()) {
         line();
     }
     // Generate global declarations in the order they appear in the module.
@@ -94,13 +94,9 @@
     return true;
 }
 
-bool GeneratorImpl::EmitEnableDirective(const ast::Enable::ExtensionKind ext) {
+bool GeneratorImpl::EmitEnable(const ast::Enable* enable) {
     auto out = line();
-    auto extension = ast::Enable::KindToName(ext);
-    if (extension == "") {
-        return false;
-    }
-    out << "enable " << extension << ";";
+    out << "enable " << enable->extension << ";";
     return true;
 }
 
diff --git a/src/tint/writer/wgsl/generator_impl.h b/src/tint/writer/wgsl/generator_impl.h
index 8473b4f..a17e2da 100644
--- a/src/tint/writer/wgsl/generator_impl.h
+++ b/src/tint/writer/wgsl/generator_impl.h
@@ -53,9 +53,9 @@
     bool Generate();
 
     /// Handles generating a enable directive
-    /// @param ext the extension kind in the enable directive to generate
+    /// @param enable the enable node
     /// @returns true if the enable directive was emitted
-    bool EmitEnableDirective(const ast::Enable::ExtensionKind ext);
+    bool EmitEnable(const ast::Enable* enable);
     /// Handles generating a declared type
     /// @param ty the declared type to generate
     /// @returns true if the declared type was emitted
diff --git a/src/tint/writer/wgsl/generator_impl_enable_test.cc b/src/tint/writer/wgsl/generator_impl_enable_test.cc
index f9de371..503a9a0 100644
--- a/src/tint/writer/wgsl/generator_impl_enable_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_enable_test.cc
@@ -20,10 +20,12 @@
 using WgslGeneratorImplTest = TestHelper;
 
 TEST_F(WgslGeneratorImplTest, Emit_Enable) {
+    auto* enable = Enable(ast::Extension::kF16);
+
     GeneratorImpl& gen = Build();
 
-    ASSERT_TRUE(gen.EmitEnableDirective(ast::Enable::ExtensionKind::kInternalExtensionForTesting));
-    EXPECT_EQ(gen.result(), R"(enable InternalExtensionForTesting;
+    ASSERT_TRUE(gen.EmitEnable(enable));
+    EXPECT_EQ(gen.result(), R"(enable f16;
 )");
 }
 
diff --git a/test/tint/BUILD.gn b/test/tint/BUILD.gn
index a66428b..c89e1b5 100644
--- a/test/tint/BUILD.gn
+++ b/test/tint/BUILD.gn
@@ -164,6 +164,7 @@
     "../../src/tint/ast/depth_texture_test.cc",
     "../../src/tint/ast/discard_statement_test.cc",
     "../../src/tint/ast/enable_test.cc",
+    "../../src/tint/ast/extension_test.cc",
     "../../src/tint/ast/external_texture_test.cc",
     "../../src/tint/ast/f16_test.cc",
     "../../src/tint/ast/f32_test.cc",
diff --git a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl b/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl
deleted file mode 100644
index 9ed8f61..0000000
--- a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright 2022 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-// Enable a void internal extension
-enable InternalExtensionForTesting;
-
-fn bar() {
-}
-
-@stage(fragment)
-fn main() -> @location(0) vec4<f32> {
-    var a : vec2<f32> = vec2<f32>();
-    bar();
-    return vec4<f32>(0.4, 0.4, 0.8, 1.0);
-}
diff --git a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.spvasm b/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.spvasm
deleted file mode 100644
index f07e733..0000000
--- a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.spvasm
+++ /dev/null
@@ -1,47 +0,0 @@
-; SPIR-V
-; Version: 1.3
-; Generator: Google Tint Compiler; 0
-; Bound: 25
-; Schema: 0
-               OpCapability Shader
-               OpMemoryModel Logical GLSL450
-               OpEntryPoint Fragment %main "main" %value
-               OpExecutionMode %main OriginUpperLeft
-               OpName %value "value"
-               OpName %bar "bar"
-               OpName %main_inner "main_inner"
-               OpName %a "a"
-               OpName %main "main"
-               OpDecorate %value Location 0
-      %float = OpTypeFloat 32
-    %v4float = OpTypeVector %float 4
-%_ptr_Output_v4float = OpTypePointer Output %v4float
-          %5 = OpConstantNull %v4float
-      %value = OpVariable %_ptr_Output_v4float Output %5
-       %void = OpTypeVoid
-          %6 = OpTypeFunction %void
-         %10 = OpTypeFunction %v4float
-    %v2float = OpTypeVector %float 2
-         %14 = OpConstantNull %v2float
-%_ptr_Function_v2float = OpTypePointer Function %v2float
-%float_0_400000006 = OpConstant %float 0.400000006
-%float_0_800000012 = OpConstant %float 0.800000012
-    %float_1 = OpConstant %float 1
-         %21 = OpConstantComposite %v4float %float_0_400000006 %float_0_400000006 %float_0_800000012 %float_1
-        %bar = OpFunction %void None %6
-          %9 = OpLabel
-               OpReturn
-               OpFunctionEnd
- %main_inner = OpFunction %v4float None %10
-         %12 = OpLabel
-          %a = OpVariable %_ptr_Function_v2float Function %14
-               OpStore %a %14
-         %17 = OpFunctionCall %void %bar
-               OpReturnValue %21
-               OpFunctionEnd
-       %main = OpFunction %void None %6
-         %23 = OpLabel
-         %24 = OpFunctionCall %v4float %main_inner
-               OpStore %value %24
-               OpReturn
-               OpFunctionEnd
diff --git a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.wgsl b/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.wgsl
deleted file mode 100644
index 987fb57..0000000
--- a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.wgsl
+++ /dev/null
@@ -1,11 +0,0 @@
-enable InternalExtensionForTesting;
-
-fn bar() {
-}
-
-@stage(fragment)
-fn main() -> @location(0) vec4<f32> {
-  var a : vec2<f32> = vec2<f32>();
-  bar();
-  return vec4<f32>(0.400000006, 0.400000006, 0.800000012, 1.0);
-}
diff --git a/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl b/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl
deleted file mode 100644
index c20b453..0000000
--- a/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright 2022 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-// Enable a void internal extension for multiple times
-enable InternalExtensionForTesting;
-enable InternalExtensionForTesting;
-enable InternalExtensionForTesting;
-
-fn bar() {
-}
-
-@stage(fragment)
-fn main() -> @location(0) vec4<f32> {
-    var a : vec2<f32> = vec2<f32>();
-    bar();
-    return vec4<f32>(0.4, 0.4, 0.8, 1.0);
-}
diff --git a/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl.expected.glsl b/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl.expected.glsl
deleted file mode 100644
index 995583f..0000000
--- a/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl.expected.glsl
+++ /dev/null
@@ -1,18 +0,0 @@
-#version 310 es
-precision mediump float;
-
-layout(location = 0) out vec4 value;
-void bar() {
-}
-
-vec4 tint_symbol() {
-  vec2 a = vec2(0.0f, 0.0f);
-  bar();
-  return vec4(0.400000006f, 0.400000006f, 0.800000012f, 1.0f);
-}
-
-void main() {
-  vec4 inner_result = tint_symbol();
-  value = inner_result;
-  return;
-}
diff --git a/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl.expected.hlsl b/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl.expected.hlsl
deleted file mode 100644
index 93c7fa7..0000000
--- a/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl.expected.hlsl
+++ /dev/null
@@ -1,19 +0,0 @@
-void bar() {
-}
-
-struct tint_symbol {
-  float4 value : SV_Target0;
-};
-
-float4 main_inner() {
-  float2 a = float2(0.0f, 0.0f);
-  bar();
-  return float4(0.400000006f, 0.400000006f, 0.800000012f, 1.0f);
-}
-
-tint_symbol main() {
-  const float4 inner_result = main_inner();
-  tint_symbol wrapper_result = (tint_symbol)0;
-  wrapper_result.value = inner_result;
-  return wrapper_result;
-}
diff --git a/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl.expected.msl b/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl.expected.msl
deleted file mode 100644
index d7fde53..0000000
--- a/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl.expected.msl
+++ /dev/null
@@ -1,23 +0,0 @@
-#include <metal_stdlib>
-
-using namespace metal;
-void bar() {
-}
-
-struct tint_symbol_1 {
-  float4 value [[color(0)]];
-};
-
-float4 tint_symbol_inner() {
-  float2 a = float2();
-  bar();
-  return float4(0.400000006f, 0.400000006f, 0.800000012f, 1.0f);
-}
-
-fragment tint_symbol_1 tint_symbol() {
-  float4 const inner_result = tint_symbol_inner();
-  tint_symbol_1 wrapper_result = {};
-  wrapper_result.value = inner_result;
-  return wrapper_result;
-}
-
diff --git a/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl.expected.spvasm b/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl.expected.spvasm
deleted file mode 100644
index f07e733..0000000
--- a/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl.expected.spvasm
+++ /dev/null
@@ -1,47 +0,0 @@
-; SPIR-V
-; Version: 1.3
-; Generator: Google Tint Compiler; 0
-; Bound: 25
-; Schema: 0
-               OpCapability Shader
-               OpMemoryModel Logical GLSL450
-               OpEntryPoint Fragment %main "main" %value
-               OpExecutionMode %main OriginUpperLeft
-               OpName %value "value"
-               OpName %bar "bar"
-               OpName %main_inner "main_inner"
-               OpName %a "a"
-               OpName %main "main"
-               OpDecorate %value Location 0
-      %float = OpTypeFloat 32
-    %v4float = OpTypeVector %float 4
-%_ptr_Output_v4float = OpTypePointer Output %v4float
-          %5 = OpConstantNull %v4float
-      %value = OpVariable %_ptr_Output_v4float Output %5
-       %void = OpTypeVoid
-          %6 = OpTypeFunction %void
-         %10 = OpTypeFunction %v4float
-    %v2float = OpTypeVector %float 2
-         %14 = OpConstantNull %v2float
-%_ptr_Function_v2float = OpTypePointer Function %v2float
-%float_0_400000006 = OpConstant %float 0.400000006
-%float_0_800000012 = OpConstant %float 0.800000012
-    %float_1 = OpConstant %float 1
-         %21 = OpConstantComposite %v4float %float_0_400000006 %float_0_400000006 %float_0_800000012 %float_1
-        %bar = OpFunction %void None %6
-          %9 = OpLabel
-               OpReturn
-               OpFunctionEnd
- %main_inner = OpFunction %v4float None %10
-         %12 = OpLabel
-          %a = OpVariable %_ptr_Function_v2float Function %14
-               OpStore %a %14
-         %17 = OpFunctionCall %void %bar
-               OpReturnValue %21
-               OpFunctionEnd
-       %main = OpFunction %void None %6
-         %23 = OpLabel
-         %24 = OpFunctionCall %v4float %main_inner
-               OpStore %value %24
-               OpReturn
-               OpFunctionEnd
diff --git a/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl.expected.wgsl b/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl.expected.wgsl
deleted file mode 100644
index 987fb57..0000000
--- a/test/tint/extensions/InternalExtensionForTesting/simple_with_duplicated_InternalExtensionForTesting.wgsl.expected.wgsl
+++ /dev/null
@@ -1,11 +0,0 @@
-enable InternalExtensionForTesting;
-
-fn bar() {
-}
-
-@stage(fragment)
-fn main() -> @location(0) vec4<f32> {
-  var a : vec2<f32> = vec2<f32>();
-  bar();
-  return vec4<f32>(0.400000006, 0.400000006, 0.800000012, 1.0);
-}
diff --git a/test/tint/extensions/parsing/basic.wgsl b/test/tint/extensions/parsing/basic.wgsl
new file mode 100644
index 0000000..d1d3017
--- /dev/null
+++ b/test/tint/extensions/parsing/basic.wgsl
@@ -0,0 +1,7 @@
+// Enable a void internal extension
+enable f16;
+
+@stage(fragment)
+fn main() -> @location(0) vec4<f32> {
+    return vec4<f32>(0.1, 0.2, 0.3, 0.4);
+}
diff --git a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.glsl b/test/tint/extensions/parsing/basic.wgsl.expected.glsl
similarity index 61%
rename from test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.glsl
rename to test/tint/extensions/parsing/basic.wgsl.expected.glsl
index 995583f..b094b6e 100644
--- a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.glsl
+++ b/test/tint/extensions/parsing/basic.wgsl.expected.glsl
@@ -2,13 +2,8 @@
 precision mediump float;
 
 layout(location = 0) out vec4 value;
-void bar() {
-}
-
 vec4 tint_symbol() {
-  vec2 a = vec2(0.0f, 0.0f);
-  bar();
-  return vec4(0.400000006f, 0.400000006f, 0.800000012f, 1.0f);
+  return vec4(0.100000001f, 0.200000003f, 0.300000012f, 0.400000006f);
 }
 
 void main() {
diff --git a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.hlsl b/test/tint/extensions/parsing/basic.wgsl.expected.hlsl
similarity index 67%
rename from test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.hlsl
rename to test/tint/extensions/parsing/basic.wgsl.expected.hlsl
index 93c7fa7..1af441a 100644
--- a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.hlsl
+++ b/test/tint/extensions/parsing/basic.wgsl.expected.hlsl
@@ -1,14 +1,9 @@
-void bar() {
-}
-
 struct tint_symbol {
   float4 value : SV_Target0;
 };
 
 float4 main_inner() {
-  float2 a = float2(0.0f, 0.0f);
-  bar();
-  return float4(0.400000006f, 0.400000006f, 0.800000012f, 1.0f);
+  return float4(0.100000001f, 0.200000003f, 0.300000012f, 0.400000006f);
 }
 
 tint_symbol main() {
diff --git a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.msl b/test/tint/extensions/parsing/basic.wgsl.expected.msl
similarity index 74%
rename from test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.msl
rename to test/tint/extensions/parsing/basic.wgsl.expected.msl
index d7fde53..dc91926 100644
--- a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.msl
+++ b/test/tint/extensions/parsing/basic.wgsl.expected.msl
@@ -1,17 +1,12 @@
 #include <metal_stdlib>
 
 using namespace metal;
-void bar() {
-}
-
 struct tint_symbol_1 {
   float4 value [[color(0)]];
 };
 
 float4 tint_symbol_inner() {
-  float2 a = float2();
-  bar();
-  return float4(0.400000006f, 0.400000006f, 0.800000012f, 1.0f);
+  return float4(0.100000001f, 0.200000003f, 0.300000012f, 0.400000006f);
 }
 
 fragment tint_symbol_1 tint_symbol() {
diff --git a/test/tint/extensions/parsing/basic.wgsl.expected.spvasm b/test/tint/extensions/parsing/basic.wgsl.expected.spvasm
new file mode 100644
index 0000000..225da82
--- /dev/null
+++ b/test/tint/extensions/parsing/basic.wgsl.expected.spvasm
@@ -0,0 +1,36 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 19
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main" %value
+               OpExecutionMode %main OriginUpperLeft
+               OpName %value "value"
+               OpName %main_inner "main_inner"
+               OpName %main "main"
+               OpDecorate %value Location 0
+      %float = OpTypeFloat 32
+    %v4float = OpTypeVector %float 4
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+          %5 = OpConstantNull %v4float
+      %value = OpVariable %_ptr_Output_v4float Output %5
+          %6 = OpTypeFunction %v4float
+%float_0_100000001 = OpConstant %float 0.100000001
+%float_0_200000003 = OpConstant %float 0.200000003
+%float_0_300000012 = OpConstant %float 0.300000012
+%float_0_400000006 = OpConstant %float 0.400000006
+         %13 = OpConstantComposite %v4float %float_0_100000001 %float_0_200000003 %float_0_300000012 %float_0_400000006
+       %void = OpTypeVoid
+         %14 = OpTypeFunction %void
+ %main_inner = OpFunction %v4float None %6
+          %8 = OpLabel
+               OpReturnValue %13
+               OpFunctionEnd
+       %main = OpFunction %void None %14
+         %17 = OpLabel
+         %18 = OpFunctionCall %v4float %main_inner
+               OpStore %value %18
+               OpReturn
+               OpFunctionEnd
diff --git a/test/tint/extensions/parsing/basic.wgsl.expected.wgsl b/test/tint/extensions/parsing/basic.wgsl.expected.wgsl
new file mode 100644
index 0000000..c084b23
--- /dev/null
+++ b/test/tint/extensions/parsing/basic.wgsl.expected.wgsl
@@ -0,0 +1,6 @@
+enable f16;
+
+@stage(fragment)
+fn main() -> @location(0) vec4<f32> {
+  return vec4<f32>(0.100000001, 0.200000003, 0.300000012, 0.400000006);
+}
diff --git a/test/tint/extensions/parsing/duplicated_extensions.wgsl b/test/tint/extensions/parsing/duplicated_extensions.wgsl
new file mode 100644
index 0000000..d72f18d
--- /dev/null
+++ b/test/tint/extensions/parsing/duplicated_extensions.wgsl
@@ -0,0 +1,9 @@
+// Enable a void internal extension for multiple times
+enable f16;
+enable f16;
+enable f16;
+
+@stage(fragment)
+fn main() -> @location(0) vec4<f32> {
+    return vec4<f32>(0.1, 0.2, 0.3, 0.4);
+}
diff --git a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.glsl b/test/tint/extensions/parsing/duplicated_extensions.wgsl.expected.glsl
similarity index 61%
copy from test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.glsl
copy to test/tint/extensions/parsing/duplicated_extensions.wgsl.expected.glsl
index 995583f..b094b6e 100644
--- a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.glsl
+++ b/test/tint/extensions/parsing/duplicated_extensions.wgsl.expected.glsl
@@ -2,13 +2,8 @@
 precision mediump float;
 
 layout(location = 0) out vec4 value;
-void bar() {
-}
-
 vec4 tint_symbol() {
-  vec2 a = vec2(0.0f, 0.0f);
-  bar();
-  return vec4(0.400000006f, 0.400000006f, 0.800000012f, 1.0f);
+  return vec4(0.100000001f, 0.200000003f, 0.300000012f, 0.400000006f);
 }
 
 void main() {
diff --git a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.hlsl b/test/tint/extensions/parsing/duplicated_extensions.wgsl.expected.hlsl
similarity index 67%
copy from test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.hlsl
copy to test/tint/extensions/parsing/duplicated_extensions.wgsl.expected.hlsl
index 93c7fa7..1af441a 100644
--- a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.hlsl
+++ b/test/tint/extensions/parsing/duplicated_extensions.wgsl.expected.hlsl
@@ -1,14 +1,9 @@
-void bar() {
-}
-
 struct tint_symbol {
   float4 value : SV_Target0;
 };
 
 float4 main_inner() {
-  float2 a = float2(0.0f, 0.0f);
-  bar();
-  return float4(0.400000006f, 0.400000006f, 0.800000012f, 1.0f);
+  return float4(0.100000001f, 0.200000003f, 0.300000012f, 0.400000006f);
 }
 
 tint_symbol main() {
diff --git a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.msl b/test/tint/extensions/parsing/duplicated_extensions.wgsl.expected.msl
similarity index 74%
copy from test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.msl
copy to test/tint/extensions/parsing/duplicated_extensions.wgsl.expected.msl
index d7fde53..dc91926 100644
--- a/test/tint/extensions/InternalExtensionForTesting/simple_with_InternalExtensionForTesting.wgsl.expected.msl
+++ b/test/tint/extensions/parsing/duplicated_extensions.wgsl.expected.msl
@@ -1,17 +1,12 @@
 #include <metal_stdlib>
 
 using namespace metal;
-void bar() {
-}
-
 struct tint_symbol_1 {
   float4 value [[color(0)]];
 };
 
 float4 tint_symbol_inner() {
-  float2 a = float2();
-  bar();
-  return float4(0.400000006f, 0.400000006f, 0.800000012f, 1.0f);
+  return float4(0.100000001f, 0.200000003f, 0.300000012f, 0.400000006f);
 }
 
 fragment tint_symbol_1 tint_symbol() {
diff --git a/test/tint/extensions/parsing/duplicated_extensions.wgsl.expected.spvasm b/test/tint/extensions/parsing/duplicated_extensions.wgsl.expected.spvasm
new file mode 100644
index 0000000..225da82
--- /dev/null
+++ b/test/tint/extensions/parsing/duplicated_extensions.wgsl.expected.spvasm
@@ -0,0 +1,36 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 19
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main" %value
+               OpExecutionMode %main OriginUpperLeft
+               OpName %value "value"
+               OpName %main_inner "main_inner"
+               OpName %main "main"
+               OpDecorate %value Location 0
+      %float = OpTypeFloat 32
+    %v4float = OpTypeVector %float 4
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+          %5 = OpConstantNull %v4float
+      %value = OpVariable %_ptr_Output_v4float Output %5
+          %6 = OpTypeFunction %v4float
+%float_0_100000001 = OpConstant %float 0.100000001
+%float_0_200000003 = OpConstant %float 0.200000003
+%float_0_300000012 = OpConstant %float 0.300000012
+%float_0_400000006 = OpConstant %float 0.400000006
+         %13 = OpConstantComposite %v4float %float_0_100000001 %float_0_200000003 %float_0_300000012 %float_0_400000006
+       %void = OpTypeVoid
+         %14 = OpTypeFunction %void
+ %main_inner = OpFunction %v4float None %6
+          %8 = OpLabel
+               OpReturnValue %13
+               OpFunctionEnd
+       %main = OpFunction %void None %14
+         %17 = OpLabel
+         %18 = OpFunctionCall %v4float %main_inner
+               OpStore %value %18
+               OpReturn
+               OpFunctionEnd
diff --git a/test/tint/extensions/parsing/duplicated_extensions.wgsl.expected.wgsl b/test/tint/extensions/parsing/duplicated_extensions.wgsl.expected.wgsl
new file mode 100644
index 0000000..be5f381
--- /dev/null
+++ b/test/tint/extensions/parsing/duplicated_extensions.wgsl.expected.wgsl
@@ -0,0 +1,8 @@
+enable f16;
+enable f16;
+enable f16;
+
+@stage(fragment)
+fn main() -> @location(0) vec4<f32> {
+  return vec4<f32>(0.100000001, 0.200000003, 0.300000012, 0.400000006);
+}