tint: Support multiple extensions per 'enable'

Fixed: tint:1865
Change-Id: I245bd36b12ff23977c2e69deee27f976db820849
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/123900
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
Commit-Queue: Dan Sinclair <dsinclair@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index c8d89e4..11d98bd 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -495,6 +495,7 @@
     "ast/discard_statement.h",
     "ast/enable.h",
     "ast/expression.h",
+    "ast/extension.h",
     "ast/float_literal_expression.h",
     "ast/for_loop_statement.h",
     "ast/function.h",
@@ -581,6 +582,7 @@
     "ast/discard_statement.cc",
     "ast/enable.cc",
     "ast/expression.cc",
+    "ast/extension.cc",
     "ast/float_literal_expression.cc",
     "ast/for_loop_statement.cc",
     "ast/function.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 0c2da7a..07f7aa8 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -134,6 +134,8 @@
   ast/enable.h
   ast/expression.cc
   ast/expression.h
+  ast/extension.cc
+  ast/extension.h
   ast/float_literal_expression.cc
   ast/float_literal_expression.h
   ast/for_loop_statement.cc
diff --git a/src/tint/ast/enable.cc b/src/tint/ast/enable.cc
index 6087a3e..bb2b3b6 100644
--- a/src/tint/ast/enable.cc
+++ b/src/tint/ast/enable.cc
@@ -20,15 +20,29 @@
 
 namespace tint::ast {
 
-Enable::Enable(ProgramID pid, NodeID nid, const Source& src, builtin::Extension ext)
-    : Base(pid, nid, src), extension(ext) {}
+Enable::Enable(ProgramID pid,
+               NodeID nid,
+               const Source& src,
+               utils::VectorRef<const Extension*> exts)
+    : Base(pid, nid, src), extensions(std::move(exts)) {}
 
 Enable::Enable(Enable&&) = default;
 
 Enable::~Enable() = default;
 
+bool Enable::HasExtension(builtin::Extension ext) const {
+    for (auto* e : extensions) {
+        if (e->name == ext) {
+            return true;
+        }
+    }
+    return false;
+}
+
 const Enable* Enable::Clone(CloneContext* ctx) const {
     auto src = ctx->Clone(source);
-    return ctx->dst->create<Enable>(src, extension);
+    auto exts = ctx->Clone(extensions);
+    return ctx->dst->create<Enable>(src, std::move(exts));
 }
+
 }  // namespace tint::ast
diff --git a/src/tint/ast/enable.h b/src/tint/ast/enable.h
index 0c64df9..d87d12f 100644
--- a/src/tint/ast/enable.h
+++ b/src/tint/ast/enable.h
@@ -19,8 +19,7 @@
 #include <utility>
 #include <vector>
 
-#include "src/tint/ast/node.h"
-#include "src/tint/builtin/extension.h"
+#include "src/tint/ast/extension.h"
 
 namespace tint::ast {
 
@@ -35,21 +34,24 @@
     /// @param pid the identifier of the program that owns this node
     /// @param nid the unique node identifier
     /// @param src the source of this node
-    /// @param ext the extension
-    Enable(ProgramID pid, NodeID nid, const Source& src, builtin::Extension ext);
+    /// @param exts the extensions being enabled by this directive
+    Enable(ProgramID pid, NodeID nid, const Source& src, utils::VectorRef<const Extension*> exts);
     /// Move constructor
     Enable(Enable&&);
 
     ~Enable() override;
 
-    /// Clones this node and all transitive child nodes using the `CloneContext`
-    /// `ctx`.
+    /// @param ext the extension to search for
+    /// @returns true if this Enable lists the given extension
+    bool HasExtension(builtin::Extension ext) const;
+
+    /// Clones this node and all transitive child nodes using the `CloneContext` `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
     const Enable* Clone(CloneContext* ctx) const override;
 
-    /// The extension name
-    const builtin::Extension extension;
+    /// The extensions being enabled by this directive
+    const utils::Vector<const Extension*, 4> extensions;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/ast/enable_test.cc b/src/tint/ast/enable_test.cc
index 5622207..20105bc 100644
--- a/src/tint/ast/enable_test.cc
+++ b/src/tint/ast/enable_test.cc
@@ -27,7 +27,14 @@
     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->extension, builtin::Extension::kF16);
+    ASSERT_EQ(ext->extensions.Length(), 1u);
+    EXPECT_EQ(ext->extensions[0]->name, builtin::Extension::kF16);
+}
+
+TEST_F(EnableTest, HasExtension) {
+    auto* ext = Enable(Source{{{20, 2}, {20, 5}}}, builtin::Extension::kF16);
+    EXPECT_TRUE(ext->HasExtension(builtin::Extension::kF16));
+    EXPECT_FALSE(ext->HasExtension(builtin::Extension::kChromiumDisableUniformityAnalysis));
 }
 
 }  // namespace
diff --git a/src/tint/ast/extension.cc b/src/tint/ast/extension.cc
new file mode 100644
index 0000000..a45c704
--- /dev/null
+++ b/src/tint/ast/extension.cc
@@ -0,0 +1,40 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/extension.h"
+
+#include "src/tint/program_builder.h"
+
+//! @cond Doxygen_Suppress
+// Doxygen gets confused with tint::ast::Extension and tint::builtin::Extension
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Extension);
+
+namespace tint::ast {
+
+Extension::Extension(ProgramID pid, NodeID nid, const Source& src, builtin::Extension ext)
+    : Base(pid, nid, src), name(ext) {}
+
+Extension::Extension(Extension&&) = default;
+
+Extension::~Extension() = default;
+
+const Extension* Extension::Clone(CloneContext* ctx) const {
+    auto src = ctx->Clone(source);
+    return ctx->dst->create<Extension>(src, name);
+}
+
+}  // namespace tint::ast
+
+//! @endcond
diff --git a/src/tint/ast/extension.h b/src/tint/ast/extension.h
new file mode 100644
index 0000000..93f3baa
--- /dev/null
+++ b/src/tint/ast/extension.h
@@ -0,0 +1,52 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_EXTENSION_H_
+#define SRC_TINT_AST_EXTENSION_H_
+
+#include "src/tint/ast/node.h"
+#include "src/tint/builtin/extension.h"
+
+namespace tint::ast {
+
+/// An extension used in an "enable" directive. Example:
+/// ```
+///   enable f16;
+/// ```
+class Extension final : public Castable<Extension, Node> {
+  public:
+    /// Create a extension
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
+    /// @param src the source of this node
+    /// @param ext the extension
+    Extension(ProgramID pid, NodeID nid, const Source& src, builtin::Extension ext);
+    /// Move constructor
+    Extension(Extension&&);
+
+    ~Extension() override;
+
+    /// Clones this node and all transitive child nodes using the `CloneContext`
+    /// `ctx`.
+    /// @param ctx the clone context
+    /// @return the newly cloned node
+    const Extension* Clone(CloneContext* ctx) const override;
+
+    /// The extension name
+    const builtin::Extension name;
+};
+
+}  // namespace tint::ast
+
+#endif  // SRC_TINT_AST_EXTENSION_H_
diff --git a/src/tint/cmd/main.cc b/src/tint/cmd/main.cc
index 2daf75f..898f0ac 100644
--- a/src/tint/cmd/main.cc
+++ b/src/tint/cmd/main.cc
@@ -755,8 +755,8 @@
 
                 auto enable_list = program->AST().Enables();
                 bool dxc_require_16bit_types = false;
-                for (auto enable : enable_list) {
-                    if (enable->extension == tint::builtin::Extension::kF16) {
+                for (auto* enable : enable_list) {
+                    if (enable->HasExtension(tint::builtin::Extension::kF16)) {
                         dxc_require_16bit_types = true;
                         break;
                     }
diff --git a/src/tint/inspector/inspector.cc b/src/tint/inspector/inspector.cc
index a480356..93e5c58 100644
--- a/src/tint/inspector/inspector.cc
+++ b/src/tint/inspector/inspector.cc
@@ -571,8 +571,10 @@
     // Ast nodes for enable directive are stored within global declarations list
     auto global_decls = program_->AST().GlobalDeclarations();
     for (auto* node : global_decls) {
-        if (auto* ext = node->As<ast::Enable>()) {
-            result.push_back({utils::ToString(ext->extension), ext->source});
+        if (auto* enable = node->As<ast::Enable>()) {
+            for (auto* ext : enable->extensions) {
+                result.push_back({utils::ToString(ext->name), ext->source});
+            }
         }
     }
 
diff --git a/src/tint/program_builder.h b/src/tint/program_builder.h
index d5576b8..31f566b 100644
--- a/src/tint/program_builder.h
+++ b/src/tint/program_builder.h
@@ -2048,20 +2048,22 @@
     }
 
     /// Adds the extension to the list of enable directives at the top of the module.
-    /// @param ext the extension to enable
+    /// @param extension the extension to enable
     /// @return an `ast::Enable` enabling the given extension.
-    const ast::Enable* Enable(builtin::Extension ext) {
-        auto* enable = create<ast::Enable>(ext);
+    const ast::Enable* Enable(builtin::Extension extension) {
+        auto* ext = create<ast::Extension>(extension);
+        auto* enable = create<ast::Enable>(utils::Vector{ext});
         AST().AddEnable(enable);
         return enable;
     }
 
     /// Adds the extension to the list of enable directives at the top of the module.
     /// @param source the enable source
-    /// @param ext the extension to enable
+    /// @param extension the extension to enable
     /// @return an `ast::Enable` enabling the given extension.
-    const ast::Enable* Enable(const Source& source, builtin::Extension ext) {
-        auto* enable = create<ast::Enable>(source, ext);
+    const ast::Enable* Enable(const Source& source, builtin::Extension extension) {
+        auto* ext = create<ast::Extension>(source, extension);
+        auto* enable = create<ast::Enable>(source, utils::Vector{ext});
         AST().AddEnable(enable);
         return enable;
     }
diff --git a/src/tint/reader/wgsl/parser_impl.cc b/src/tint/reader/wgsl/parser_impl.cc
index c1a379e..6864084 100644
--- a/src/tint/reader/wgsl/parser_impl.cc
+++ b/src/tint/reader/wgsl/parser_impl.cc
@@ -390,53 +390,52 @@
     return decl;
 }
 
-// enable_directive
-//  : enable name SEMICLON
+// enable_directive :
+// | 'enable' identifier (COMMA identifier)* COMMA? SEMICOLON
 Maybe<Void> ParserImpl::enable_directive() {
-    auto decl = sync(Token::Type::kSemicolon, [&]() -> Maybe<Void> {
+    return sync(Token::Type::kSemicolon, [&]() -> Maybe<Void> {
+        MultiTokenSource decl_source(this);
         if (!match(Token::Type::kEnable)) {
             return Failure::kNoMatch;
         }
 
-        // Match the extension name.
-        auto& t = peek();
-        if (handle_error(t)) {
-            // The token might itself be an error.
-            return Failure::kErrored;
-        }
-
-        if (t.Is(Token::Type::kParenLeft)) {
+        if (peek_is(Token::Type::kParenLeft)) {
             // A common error case is writing `enable(foo);` instead of `enable foo;`.
             synchronized_ = false;
-            return add_error(t.source(), "enable directives don't take parenthesis");
+            return add_error(peek().source(), "enable directives don't take parenthesis");
         }
 
-        auto ext = expect_enum("extension", builtin::ParseExtension, builtin::kExtensionStrings);
-        if (ext.errored) {
-            return Failure::kErrored;
+        utils::Vector<const ast::Extension*, 4> extensions;
+        while (continue_parsing()) {
+            Source ext_src = peek().source();
+            auto ext =
+                expect_enum("extension", builtin::ParseExtension, builtin::kExtensionStrings);
+            if (ext.errored) {
+                return Failure::kErrored;
+            }
+            extensions.Push(create<ast::Extension>(ext_src, ext.value));
+
+            if (!match(Token::Type::kComma)) {
+                break;
+            }
+            if (peek_is(Token::Type::kSemicolon)) {
+                break;
+            }
         }
 
         if (!expect("enable directive", Token::Type::kSemicolon)) {
             return Failure::kErrored;
         }
-        builder_.AST().AddEnable(create<ast::Enable>(t.source(), ext.value));
+
+        builder_.AST().AddEnable(create<ast::Enable>(decl_source.Source(), std::move(extensions)));
         return kSuccess;
     });
-
-    if (decl.errored) {
-        return Failure::kErrored;
-    }
-    if (decl.matched) {
-        return kSuccess;
-    }
-
-    return Failure::kNoMatch;
 }
 
 // requires_directive
-//  : require identifier (COMMA identifier)? SEMICLON
+//  : require identifier (COMMA identifier)* COMMA? SEMICOLON
 Maybe<Void> ParserImpl::requires_directive() {
-    auto decl = sync(Token::Type::kSemicolon, [&]() -> Maybe<Void> {
+    return sync(Token::Type::kSemicolon, [&]() -> Maybe<Void> {
         if (!match(Token::Type::kRequires)) {
             return Failure::kNoMatch;
         }
@@ -483,15 +482,6 @@
         // conditional.
         return add_error(t.source(), "missing feature names in requires directive");
     });
-
-    if (decl.errored) {
-        return Failure::kErrored;
-    }
-    if (decl.matched) {
-        return kSuccess;
-    }
-
-    return Failure::kNoMatch;
 }
 
 // global_decl
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 7ad39c2..3c63a02 100644
--- a/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
@@ -22,7 +22,7 @@
 using EnableDirectiveTest = ParserImplTest;
 
 // Test a valid enable directive.
-TEST_F(EnableDirectiveTest, Valid) {
+TEST_F(EnableDirectiveTest, Single) {
     auto p = parser("enable f16;");
     p->enable_directive();
     EXPECT_FALSE(p->has_error()) << p->error();
@@ -30,13 +30,105 @@
     auto& ast = program.AST();
     ASSERT_EQ(ast.Enables().Length(), 1u);
     auto* enable = ast.Enables()[0];
-    EXPECT_EQ(enable->extension, builtin::Extension::kF16);
+    EXPECT_EQ(enable->source.range.begin.line, 1u);
+    EXPECT_EQ(enable->source.range.begin.column, 1u);
+    EXPECT_EQ(enable->source.range.end.line, 1u);
+    EXPECT_EQ(enable->source.range.end.column, 12u);
+    ASSERT_EQ(enable->extensions.Length(), 1u);
+    EXPECT_EQ(enable->extensions[0]->name, builtin::Extension::kF16);
+    EXPECT_EQ(enable->extensions[0]->source.range.begin.line, 1u);
+    EXPECT_EQ(enable->extensions[0]->source.range.begin.column, 8u);
+    EXPECT_EQ(enable->extensions[0]->source.range.end.line, 1u);
+    EXPECT_EQ(enable->extensions[0]->source.range.end.column, 11u);
+    ASSERT_EQ(ast.GlobalDeclarations().Length(), 1u);
+    EXPECT_EQ(ast.GlobalDeclarations()[0], enable);
+}
+
+// Test a valid enable directive.
+TEST_F(EnableDirectiveTest, SingleTrailingComma) {
+    auto p = parser("enable f16, ;");
+    p->enable_directive();
+    EXPECT_FALSE(p->has_error()) << p->error();
+    auto program = p->program();
+    auto& ast = program.AST();
+    ASSERT_EQ(ast.Enables().Length(), 1u);
+    auto* enable = ast.Enables()[0];
+    EXPECT_EQ(enable->source.range.begin.line, 1u);
+    EXPECT_EQ(enable->source.range.begin.column, 1u);
+    EXPECT_EQ(enable->source.range.end.line, 1u);
+    EXPECT_EQ(enable->source.range.end.column, 14u);
+    ASSERT_EQ(enable->extensions.Length(), 1u);
+    EXPECT_EQ(enable->extensions[0]->name, builtin::Extension::kF16);
+    EXPECT_EQ(enable->extensions[0]->source.range.begin.line, 1u);
+    EXPECT_EQ(enable->extensions[0]->source.range.begin.column, 8u);
+    EXPECT_EQ(enable->extensions[0]->source.range.end.line, 1u);
+    EXPECT_EQ(enable->extensions[0]->source.range.end.column, 11u);
+    ASSERT_EQ(ast.GlobalDeclarations().Length(), 1u);
+    EXPECT_EQ(ast.GlobalDeclarations()[0], enable);
+}
+
+// Test a valid enable directive with multiple extensions.
+TEST_F(EnableDirectiveTest, Multiple) {
+    auto p =
+        parser("enable f16, chromium_disable_uniformity_analysis, chromium_experimental_dp4a;");
+    p->enable_directive();
+    EXPECT_FALSE(p->has_error()) << p->error();
+    auto program = p->program();
+    auto& ast = program.AST();
+    ASSERT_EQ(ast.Enables().Length(), 1u);
+    auto* enable = ast.Enables()[0];
+    ASSERT_EQ(enable->extensions.Length(), 3u);
+    EXPECT_EQ(enable->extensions[0]->name, builtin::Extension::kF16);
+    EXPECT_EQ(enable->extensions[0]->source.range.begin.line, 1u);
+    EXPECT_EQ(enable->extensions[0]->source.range.begin.column, 8u);
+    EXPECT_EQ(enable->extensions[0]->source.range.end.line, 1u);
+    EXPECT_EQ(enable->extensions[0]->source.range.end.column, 11u);
+    EXPECT_EQ(enable->extensions[1]->name, builtin::Extension::kChromiumDisableUniformityAnalysis);
+    EXPECT_EQ(enable->extensions[1]->source.range.begin.line, 1u);
+    EXPECT_EQ(enable->extensions[1]->source.range.begin.column, 13u);
+    EXPECT_EQ(enable->extensions[1]->source.range.end.line, 1u);
+    EXPECT_EQ(enable->extensions[1]->source.range.end.column, 49u);
+    EXPECT_EQ(enable->extensions[2]->name, builtin::Extension::kChromiumExperimentalDp4A);
+    EXPECT_EQ(enable->extensions[2]->source.range.begin.line, 1u);
+    EXPECT_EQ(enable->extensions[2]->source.range.begin.column, 51u);
+    EXPECT_EQ(enable->extensions[2]->source.range.end.line, 1u);
+    EXPECT_EQ(enable->extensions[2]->source.range.end.column, 77u);
+    ASSERT_EQ(ast.GlobalDeclarations().Length(), 1u);
+    EXPECT_EQ(ast.GlobalDeclarations()[0], enable);
+}
+
+// Test a valid enable directive with multiple extensions.
+TEST_F(EnableDirectiveTest, MultipleTrailingComma) {
+    auto p =
+        parser("enable f16, chromium_disable_uniformity_analysis, chromium_experimental_dp4a,;");
+    p->enable_directive();
+    EXPECT_FALSE(p->has_error()) << p->error();
+    auto program = p->program();
+    auto& ast = program.AST();
+    ASSERT_EQ(ast.Enables().Length(), 1u);
+    auto* enable = ast.Enables()[0];
+    ASSERT_EQ(enable->extensions.Length(), 3u);
+    EXPECT_EQ(enable->extensions[0]->name, builtin::Extension::kF16);
+    EXPECT_EQ(enable->extensions[0]->source.range.begin.line, 1u);
+    EXPECT_EQ(enable->extensions[0]->source.range.begin.column, 8u);
+    EXPECT_EQ(enable->extensions[0]->source.range.end.line, 1u);
+    EXPECT_EQ(enable->extensions[0]->source.range.end.column, 11u);
+    EXPECT_EQ(enable->extensions[1]->name, builtin::Extension::kChromiumDisableUniformityAnalysis);
+    EXPECT_EQ(enable->extensions[1]->source.range.begin.line, 1u);
+    EXPECT_EQ(enable->extensions[1]->source.range.begin.column, 13u);
+    EXPECT_EQ(enable->extensions[1]->source.range.end.line, 1u);
+    EXPECT_EQ(enable->extensions[1]->source.range.end.column, 49u);
+    EXPECT_EQ(enable->extensions[2]->name, builtin::Extension::kChromiumExperimentalDp4A);
+    EXPECT_EQ(enable->extensions[2]->source.range.begin.line, 1u);
+    EXPECT_EQ(enable->extensions[2]->source.range.begin.column, 51u);
+    EXPECT_EQ(enable->extensions[2]->source.range.end.line, 1u);
+    EXPECT_EQ(enable->extensions[2]->source.range.end.column, 77u);
     ASSERT_EQ(ast.GlobalDeclarations().Length(), 1u);
     EXPECT_EQ(ast.GlobalDeclarations()[0], enable);
 }
 
 // Test multiple enable directives for a same extension.
-TEST_F(EnableDirectiveTest, EnableMultipleTime) {
+TEST_F(EnableDirectiveTest, EnableSameLine) {
     auto p = parser(R"(
 enable f16;
 enable f16;
@@ -48,8 +140,18 @@
     ASSERT_EQ(ast.Enables().Length(), 2u);
     auto* enable_a = ast.Enables()[0];
     auto* enable_b = ast.Enables()[1];
-    EXPECT_EQ(enable_a->extension, builtin::Extension::kF16);
-    EXPECT_EQ(enable_b->extension, builtin::Extension::kF16);
+    ASSERT_EQ(enable_a->extensions.Length(), 1u);
+    EXPECT_EQ(enable_a->extensions[0]->name, builtin::Extension::kF16);
+    EXPECT_EQ(enable_a->extensions[0]->source.range.begin.line, 2u);
+    EXPECT_EQ(enable_a->extensions[0]->source.range.begin.column, 8u);
+    EXPECT_EQ(enable_a->extensions[0]->source.range.end.line, 2u);
+    EXPECT_EQ(enable_a->extensions[0]->source.range.end.column, 11u);
+    ASSERT_EQ(enable_b->extensions.Length(), 1u);
+    EXPECT_EQ(enable_b->extensions[0]->name, builtin::Extension::kF16);
+    EXPECT_EQ(enable_b->extensions[0]->source.range.begin.line, 3u);
+    EXPECT_EQ(enable_b->extensions[0]->source.range.begin.column, 8u);
+    EXPECT_EQ(enable_b->extensions[0]->source.range.end.line, 3u);
+    EXPECT_EQ(enable_b->extensions[0]->source.range.end.column, 11u);
     ASSERT_EQ(ast.GlobalDeclarations().Length(), 2u);
     EXPECT_EQ(ast.GlobalDeclarations()[0], enable_a);
     EXPECT_EQ(ast.GlobalDeclarations()[1], enable_b);
@@ -169,7 +271,12 @@
     // Accept the enable directive although it caused an error
     ASSERT_EQ(ast.Enables().Length(), 1u);
     auto* enable = ast.Enables()[0];
-    EXPECT_EQ(enable->extension, builtin::Extension::kF16);
+    ASSERT_EQ(enable->extensions.Length(), 1u);
+    EXPECT_EQ(enable->extensions[0]->name, builtin::Extension::kF16);
+    EXPECT_EQ(enable->extensions[0]->source.range.begin.line, 3u);
+    EXPECT_EQ(enable->extensions[0]->source.range.begin.column, 8u);
+    EXPECT_EQ(enable->extensions[0]->source.range.end.line, 3u);
+    EXPECT_EQ(enable->extensions[0]->source.range.end.column, 11u);
     ASSERT_EQ(ast.GlobalDeclarations().Length(), 2u);
     EXPECT_EQ(ast.GlobalDeclarations()[1], enable);
 }
@@ -189,7 +296,12 @@
     // Accept the enable directive although it cause an error
     ASSERT_EQ(ast.Enables().Length(), 1u);
     auto* enable = ast.Enables()[0];
-    EXPECT_EQ(enable->extension, builtin::Extension::kF16);
+    ASSERT_EQ(enable->extensions.Length(), 1u);
+    EXPECT_EQ(enable->extensions[0]->name, builtin::Extension::kF16);
+    EXPECT_EQ(enable->extensions[0]->source.range.begin.line, 3u);
+    EXPECT_EQ(enable->extensions[0]->source.range.begin.column, 8u);
+    EXPECT_EQ(enable->extensions[0]->source.range.end.line, 3u);
+    EXPECT_EQ(enable->extensions[0]->source.range.end.column, 11u);
     ASSERT_EQ(ast.GlobalDeclarations().Length(), 1u);
     EXPECT_EQ(ast.GlobalDeclarations()[0], enable);
 }
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 48fa4b1..2cd40e9 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -3476,7 +3476,10 @@
 }
 
 bool Resolver::Enable(const ast::Enable* enable) {
-    enabled_extensions_.Add(enable->extension);
+    for (auto* ext : enable->extensions) {
+        Mark(ext);
+        enabled_extensions_.Add(ext->name);
+    }
     return true;
 }
 
diff --git a/src/tint/transform/builtin_polyfill.cc b/src/tint/transform/builtin_polyfill.cc
index c242521..cc770ab 100644
--- a/src/tint/transform/builtin_polyfill.cc
+++ b/src/tint/transform/builtin_polyfill.cc
@@ -47,8 +47,9 @@
     State(const Program* program, const Config& config) : src(program), cfg(config) {
         has_full_ptr_params = false;
         for (auto* enable : src->AST().Enables()) {
-            if (enable->extension == builtin::Extension::kChromiumExperimentalFullPtrParameters) {
+            if (enable->HasExtension(builtin::Extension::kChromiumExperimentalFullPtrParameters)) {
                 has_full_ptr_params = true;
+                break;
             }
         }
     }
diff --git a/src/tint/transform/preserve_padding.cc b/src/tint/transform/preserve_padding.cc
index 59fd786..e8c95e3 100644
--- a/src/tint/transform/preserve_padding.cc
+++ b/src/tint/transform/preserve_padding.cc
@@ -67,8 +67,8 @@
                 },
                 [&](const ast::Enable* enable) {
                     // Check if the full pointer parameters extension is already enabled.
-                    if (enable->extension ==
-                        builtin::Extension::kChromiumExperimentalFullPtrParameters) {
+                    if (enable->HasExtension(
+                            builtin::Extension::kChromiumExperimentalFullPtrParameters)) {
                         ext_enabled = true;
                     }
                 });
diff --git a/src/tint/writer/check_supported_extensions.cc b/src/tint/writer/check_supported_extensions.cc
index bde86a5..88ddcdd 100644
--- a/src/tint/writer/check_supported_extensions.cc
+++ b/src/tint/writer/check_supported_extensions.cc
@@ -33,13 +33,14 @@
     }
 
     for (auto* enable : module.Enables()) {
-        auto ext = enable->extension;
-        if (!set.Contains(ext)) {
-            diags.add_error(diag::System::Writer,
-                            std::string(writer_name) + " backend does not support extension '" +
-                                utils::ToString(ext) + "'",
-                            enable->source);
-            return false;
+        for (auto* ext : enable->extensions) {
+            if (!set.Contains(ext->name)) {
+                diags.add_error(diag::System::Writer,
+                                std::string(writer_name) + " backend does not support extension '" +
+                                    utils::ToString(ext->name) + "'",
+                                ext->source);
+                return false;
+            }
         }
     }
     return true;
diff --git a/src/tint/writer/glsl/generator_impl.cc b/src/tint/writer/glsl/generator_impl.cc
index cdf4a15..311005b 100644
--- a/src/tint/writer/glsl/generator_impl.cc
+++ b/src/tint/writer/glsl/generator_impl.cc
@@ -347,10 +347,10 @@
     return true;
 }
 
-bool GeneratorImpl::RecordExtension(const ast::Enable* ext) {
+bool GeneratorImpl::RecordExtension(const ast::Enable* enable) {
     // Deal with extension node here, recording it within the generator for later emition.
 
-    if (ext->extension == builtin::Extension::kF16) {
+    if (enable->HasExtension(builtin::Extension::kF16)) {
         requires_f16_extension_ = true;
     }
 
diff --git a/src/tint/writer/spirv/builder_builtin_test.cc b/src/tint/writer/spirv/builder_builtin_test.cc
index 5db867a..280b878 100644
--- a/src/tint/writer/spirv/builder_builtin_test.cc
+++ b/src/tint/writer/spirv/builder_builtin_test.cc
@@ -4154,10 +4154,7 @@
 namespace DP4A_builtin_tests {
 
 TEST_F(BuiltinBuilderTest, Call_Dot4I8Packed) {
-    auto* ext =
-        create<ast::Enable>(Source{Source::Range{Source::Location{10, 2}, Source::Location{10, 5}}},
-                            builtin::Extension::kChromiumExperimentalDp4A);
-    AST().AddEnable(ext);
+    Enable(builtin::Extension::kChromiumExperimentalDp4A);
 
     auto* val1 = Var("val1", ty.u32());
     auto* val2 = Var("val2", ty.u32());
@@ -4194,10 +4191,7 @@
 }
 
 TEST_F(BuiltinBuilderTest, Call_Dot4U8Packed) {
-    auto* ext =
-        create<ast::Enable>(Source{Source::Range{Source::Location{10, 2}, Source::Location{10, 5}}},
-                            builtin::Extension::kChromiumExperimentalDp4A);
-    AST().AddEnable(ext);
+    Enable(builtin::Extension::kChromiumExperimentalDp4A);
 
     auto* val1 = Var("val1", ty.u32());
     auto* val2 = Var("val2", ty.u32());
diff --git a/src/tint/writer/syntax_tree/generator_impl.cc b/src/tint/writer/syntax_tree/generator_impl.cc
index 87d182e..02e6ff7 100644
--- a/src/tint/writer/syntax_tree/generator_impl.cc
+++ b/src/tint/writer/syntax_tree/generator_impl.cc
@@ -79,7 +79,15 @@
 }
 
 bool GeneratorImpl::EmitEnable(const ast::Enable* enable) {
-    line() << "Enable [" << enable->extension << "]";
+    auto l = line();
+    l << "Enable [";
+    for (auto* ext : enable->extensions) {
+        if (ext != enable->extensions.Front()) {
+            l << ", ";
+        }
+        l << ext->name;
+    }
+    l << "]";
     return true;
 }
 
diff --git a/src/tint/writer/wgsl/generator_impl.cc b/src/tint/writer/wgsl/generator_impl.cc
index 0421616..2b5f474 100644
--- a/src/tint/writer/wgsl/generator_impl.cc
+++ b/src/tint/writer/wgsl/generator_impl.cc
@@ -100,7 +100,14 @@
 
 bool GeneratorImpl::EmitEnable(const ast::Enable* enable) {
     auto out = line();
-    out << "enable " << enable->extension << ";";
+    out << "enable ";
+    for (auto* ext : enable->extensions) {
+        if (ext != enable->extensions.Front()) {
+            out << ", ";
+        }
+        out << ext->name;
+    }
+    out << ";";
     return true;
 }
 
diff --git a/test/tint/extensions/parsing/multiple.wgsl b/test/tint/extensions/parsing/multiple.wgsl
new file mode 100644
index 0000000..c5224f0
--- /dev/null
+++ b/test/tint/extensions/parsing/multiple.wgsl
@@ -0,0 +1,6 @@
+enable chromium_experimental_push_constant, f16;
+
+@fragment
+fn main() -> @location(0) vec4<f32> {
+    return vec4<f32>(0.1, 0.2, 0.3, 0.4);
+}
diff --git a/test/tint/extensions/parsing/multiple.wgsl.expected.dxc.hlsl b/test/tint/extensions/parsing/multiple.wgsl.expected.dxc.hlsl
new file mode 100644
index 0000000..02332f3
--- /dev/null
+++ b/test/tint/extensions/parsing/multiple.wgsl.expected.dxc.hlsl
@@ -0,0 +1,14 @@
+struct tint_symbol {
+  float4 value : SV_Target0;
+};
+
+float4 main_inner() {
+  return float4(0.10000000149011611938f, 0.20000000298023223877f, 0.30000001192092895508f, 0.40000000596046447754f);
+}
+
+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/parsing/multiple.wgsl.expected.fxc.hlsl b/test/tint/extensions/parsing/multiple.wgsl.expected.fxc.hlsl
new file mode 100644
index 0000000..02332f3
--- /dev/null
+++ b/test/tint/extensions/parsing/multiple.wgsl.expected.fxc.hlsl
@@ -0,0 +1,14 @@
+struct tint_symbol {
+  float4 value : SV_Target0;
+};
+
+float4 main_inner() {
+  return float4(0.10000000149011611938f, 0.20000000298023223877f, 0.30000001192092895508f, 0.40000000596046447754f);
+}
+
+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/parsing/multiple.wgsl.expected.glsl b/test/tint/extensions/parsing/multiple.wgsl.expected.glsl
new file mode 100644
index 0000000..37063fa
--- /dev/null
+++ b/test/tint/extensions/parsing/multiple.wgsl.expected.glsl
@@ -0,0 +1,14 @@
+#version 310 es
+#extension GL_AMD_gpu_shader_half_float : require
+precision highp float;
+
+layout(location = 0) out vec4 value;
+vec4 tint_symbol() {
+  return vec4(0.10000000149011611938f, 0.20000000298023223877f, 0.30000001192092895508f, 0.40000000596046447754f);
+}
+
+void main() {
+  vec4 inner_result = tint_symbol();
+  value = inner_result;
+  return;
+}
diff --git a/test/tint/extensions/parsing/multiple.wgsl.expected.msl b/test/tint/extensions/parsing/multiple.wgsl.expected.msl
new file mode 100644
index 0000000..8eb15e9
--- /dev/null
+++ b/test/tint/extensions/parsing/multiple.wgsl.expected.msl
@@ -0,0 +1,18 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct tint_symbol_1 {
+  float4 value [[color(0)]];
+};
+
+float4 tint_symbol_inner() {
+  return float4(0.10000000149011611938f, 0.20000000298023223877f, 0.30000001192092895508f, 0.40000000596046447754f);
+}
+
+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/parsing/multiple.wgsl.expected.spvasm b/test/tint/extensions/parsing/multiple.wgsl.expected.spvasm
new file mode 100644
index 0000000..3b3eac5
--- /dev/null
+++ b/test/tint/extensions/parsing/multiple.wgsl.expected.spvasm
@@ -0,0 +1,40 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 19
+; Schema: 0
+               OpCapability Shader
+               OpCapability Float16
+               OpCapability UniformAndStorageBuffer16BitAccess
+               OpCapability StorageBuffer16BitAccess
+               OpCapability StorageInputOutput16
+               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/multiple.wgsl.expected.wgsl b/test/tint/extensions/parsing/multiple.wgsl.expected.wgsl
new file mode 100644
index 0000000..448585d
--- /dev/null
+++ b/test/tint/extensions/parsing/multiple.wgsl.expected.wgsl
@@ -0,0 +1,6 @@
+enable chromium_experimental_push_constant, f16;
+
+@fragment
+fn main() -> @location(0) vec4<f32> {
+  return vec4<f32>(0.10000000000000000555, 0.2000000000000000111, 0.2999999999999999889, 0.4000000000000000222);
+}