Add Partial Tint Dual Source Blending Extension

Adds dual source blending extension string to Tint. Adds @index
attribute to ast and resolver. Includes basic ast and resolver tests.

Bug: dawn:1709
Change-Id: I6bf05b42978c3338e1f5f45d21bd2a3fb5fed08d
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/137281
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index efa4753..1d71177 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -577,6 +577,7 @@
     "ast/if_statement.h",
     "ast/increment_decrement_statement.h",
     "ast/index_accessor_expression.h",
+    "ast/index_attribute.h",
     "ast/int_literal_expression.h",
     "ast/internal_attribute.h",
     "ast/interpolate_attribute.h",
@@ -666,6 +667,7 @@
     "ast/if_statement.cc",
     "ast/increment_decrement_statement.cc",
     "ast/index_accessor_expression.cc",
+    "ast/index_attribute.cc",
     "ast/int_literal_expression.cc",
     "ast/internal_attribute.cc",
     "ast/interpolate_attribute.cc",
@@ -1564,6 +1566,7 @@
       "ast/if_statement_test.cc",
       "ast/increment_decrement_statement_test.cc",
       "ast/index_accessor_expression_test.cc",
+      "ast/index_attribute_test.cc",
       "ast/int_literal_expression_test.cc",
       "ast/interpolate_attribute_test.cc",
       "ast/location_attribute_test.cc",
@@ -1673,6 +1676,7 @@
       "resolver/control_block_validation_test.cc",
       "resolver/dependency_graph_test.cc",
       "resolver/diagnostic_control_test.cc",
+      "resolver/dual_source_blending_extension_test.cc",
       "resolver/entry_point_validation_test.cc",
       "resolver/evaluation_stage_test.cc",
       "resolver/expression_kind_test.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 3ac8ed4..5c0bf6f 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -158,6 +158,8 @@
   ast/increment_decrement_statement.h
   ast/index_accessor_expression.cc
   ast/index_accessor_expression.h
+  ast/index_attribute.cc
+  ast/index_attribute.h
   ast/int_literal_expression.cc
   ast/int_literal_expression.h
   ast/internal_attribute.cc
@@ -939,6 +941,7 @@
     ast/if_statement_test.cc
     ast/increment_decrement_statement_test.cc
     ast/index_accessor_expression_test.cc
+    ast/index_attribute_test.cc
     ast/int_literal_expression_test.cc
     ast/interpolate_attribute_test.cc
     ast/location_attribute_test.cc
@@ -1008,6 +1011,7 @@
     resolver/control_block_validation_test.cc
     resolver/dependency_graph_test.cc
     resolver/diagnostic_control_test.cc
+    resolver/dual_source_blending_extension_test.cc
     resolver/entry_point_validation_test.cc
     resolver/evaluation_stage_test.cc
     resolver/expression_kind_test.cc
diff --git a/src/tint/ast/index_attribute.cc b/src/tint/ast/index_attribute.cc
new file mode 100644
index 0000000..7916226
--- /dev/null
+++ b/src/tint/ast/index_attribute.cc
@@ -0,0 +1,41 @@
+// 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/index_attribute.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::IndexAttribute);
+
+namespace tint::ast {
+
+IndexAttribute::IndexAttribute(ProgramID pid, NodeID nid, const Source& src, const Expression* exp)
+    : Base(pid, nid, src), expr(exp) {}
+
+IndexAttribute::~IndexAttribute() = default;
+
+std::string IndexAttribute::Name() const {
+    return "index";
+}
+
+const IndexAttribute* IndexAttribute::Clone(CloneContext* ctx) const {
+    // Clone arguments outside of create() call to have deterministic ordering
+    auto src = ctx->Clone(source);
+    auto* expr_ = ctx->Clone(expr);
+    return ctx->dst->create<IndexAttribute>(src, expr_);
+}
+
+}  // namespace tint::ast
diff --git a/src/tint/ast/index_attribute.h b/src/tint/ast/index_attribute.h
new file mode 100644
index 0000000..e241ba6
--- /dev/null
+++ b/src/tint/ast/index_attribute.h
@@ -0,0 +1,51 @@
+// 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_INDEX_ATTRIBUTE_H_
+#define SRC_TINT_AST_INDEX_ATTRIBUTE_H_
+
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+#include "src/tint/ast/expression.h"
+
+namespace tint::ast {
+
+/// An id attribute for pipeline-overridable constants
+class IndexAttribute final : public utils::Castable<IndexAttribute, Attribute> {
+  public:
+    /// Create an index attribute.
+    /// @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 expr the numeric id expression
+    IndexAttribute(ProgramID pid, NodeID nid, const Source& src, const Expression* expr);
+    ~IndexAttribute() override;
+
+    /// @returns the WGSL name for the attribute
+    std::string Name() const override;
+
+    /// Clones this node and all transitive child nodes using the `CloneContext`
+    /// `ctx`.
+    /// @param ctx the clone context
+    /// @return the newly cloned node
+    const IndexAttribute* Clone(CloneContext* ctx) const override;
+
+    /// The id expression
+    const Expression* const expr;
+};
+
+}  // namespace tint::ast
+
+#endif  // SRC_TINT_AST_INDEX_ATTRIBUTE_H_
diff --git a/src/tint/ast/index_attribute_test.cc b/src/tint/ast/index_attribute_test.cc
new file mode 100644
index 0000000..bad37c6
--- /dev/null
+++ b/src/tint/ast/index_attribute_test.cc
@@ -0,0 +1,31 @@
+// 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/id_attribute.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint::ast {
+namespace {
+
+using namespace tint::number_suffixes;  // NOLINT
+using IndexAttributeTest = TestHelper;
+
+TEST_F(IndexAttributeTest, Creation) {
+    auto* d = Index(1_a);
+    EXPECT_TRUE(d->expr->Is<IntLiteralExpression>());
+}
+
+}  // namespace
+}  // namespace tint::ast
diff --git a/src/tint/builtin/attribute.cc b/src/tint/builtin/attribute.cc
index 33a5171..0647825 100644
--- a/src/tint/builtin/attribute.cc
+++ b/src/tint/builtin/attribute.cc
@@ -52,6 +52,9 @@
     if (str == "id") {
         return Attribute::kId;
     }
+    if (str == "index") {
+        return Attribute::kIndex;
+    }
     if (str == "interpolate") {
         return Attribute::kInterpolate;
     }
@@ -96,6 +99,8 @@
             return out << "group";
         case Attribute::kId:
             return out << "id";
+        case Attribute::kIndex:
+            return out << "index";
         case Attribute::kInterpolate:
             return out << "interpolate";
         case Attribute::kInvariant:
diff --git a/src/tint/builtin/attribute.h b/src/tint/builtin/attribute.h
index c975267..f9a1868 100644
--- a/src/tint/builtin/attribute.h
+++ b/src/tint/builtin/attribute.h
@@ -41,6 +41,7 @@
     kFragment,
     kGroup,
     kId,
+    kIndex,
     kInterpolate,
     kInvariant,
     kLocation,
@@ -61,9 +62,9 @@
 Attribute ParseAttribute(std::string_view str);
 
 constexpr const char* kAttributeStrings[] = {
-    "align",    "binding",  "builtin", "compute",     "diagnostic",
-    "fragment", "group",    "id",      "interpolate", "invariant",
-    "location", "must_use", "size",    "vertex",      "workgroup_size",
+    "align",    "binding", "builtin", "compute",        "diagnostic", "fragment",
+    "group",    "id",      "index",   "interpolate",    "invariant",  "location",
+    "must_use", "size",    "vertex",  "workgroup_size",
 };
 
 }  // namespace tint::builtin
diff --git a/src/tint/builtin/attribute_bench.cc b/src/tint/builtin/attribute_bench.cc
index a25fbf1..267a905 100644
--- a/src/tint/builtin/attribute_bench.cc
+++ b/src/tint/builtin/attribute_bench.cc
@@ -87,55 +87,62 @@
         "i",
         "rrd",
         "iG",
-        "FFnterpolate",
-        "iEtrplat",
-        "inerporrate",
+        "inFFex",
+        "iE",
+        "inrrx",
+        "index",
+        "inx",
+        "inJJD",
+        "ie",
+        "inerpklae",
+        "intrpolate",
+        "inJerpolae",
         "interpolate",
-        "inteplate",
-        "XterJJoDate",
-        "inepol8t",
-        "nvark1n",
-        "invriant",
-        "Jnvarant",
+        "interpocate",
+        "interpolaOe",
+        "__nttevvpoKKate",
+        "xnvari5n8",
+        "inFq__ant",
+        "iqqariant",
         "invariant",
-        "invaricnt",
-        "invariaOt",
-        "invttKK_ianvv",
-        "lxxcati8",
-        "Focqq__o",
-        "locaiqqn",
+        "invar6a33O",
+        "i96arQttanoo",
+        "inari66nt",
+        "lOxati6zz",
+        "locyytion",
+        "lHHtion",
         "location",
-        "loc33tio6",
-        "ltto6at9QQn",
-        "loc66tio",
-        "mOxt_u6zz",
-        "musyy_use",
-        "mHH_use",
+        "qWW4caton",
+        "locOOton",
+        "ocatiYn",
+        "m_use",
+        "mutFuse",
+        "wust_us",
         "must_use",
-        "qWW4st_se",
-        "musOO_se",
-        "ust_uYe",
-        "i",
-        "Fie",
-        "siw",
+        "Kst_sff",
+        "qusKK_use",
+        "mFsmm_3se",
+        "ize",
+        "sze",
+        "sbbb",
         "size",
-        "zff",
-        "sizqK",
-        "s3zmm",
-        "ertex",
-        "vereq",
-        "vbtbbx",
+        "iie",
+        "siqe",
+        "svvTTe",
+        "vertFFx",
+        "vrQ00P",
+        "vePtex",
         "vertex",
-        "irtex",
-        "vOOteq",
-        "vertTvvx",
-        "woFFkgroup_size",
-        "wfr00grPupsiQe",
-        "workgrouP_size",
+        "vsste77",
+        "veCtRRbb",
+        "verteXX",
+        "workgqou_siCCOOO",
+        "worsgroupsuzL",
+        "wXrkgroup_size",
         "workgroup_size",
-        "workgroup77sise",
-        "RRobbkgroupCsize",
-        "wXXrkgroup_size",
+        "workgroup_sze",
+        "wqqrOgoupize",
+        "workg22oup_size",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
diff --git a/src/tint/builtin/attribute_test.cc b/src/tint/builtin/attribute_test.cc
index 9919467..33f557e 100644
--- a/src/tint/builtin/attribute_test.cc
+++ b/src/tint/builtin/attribute_test.cc
@@ -43,21 +43,14 @@
 }
 
 static constexpr Case kValidCases[] = {
-    {"align", Attribute::kAlign},
-    {"binding", Attribute::kBinding},
-    {"builtin", Attribute::kBuiltin},
-    {"compute", Attribute::kCompute},
-    {"diagnostic", Attribute::kDiagnostic},
-    {"fragment", Attribute::kFragment},
-    {"group", Attribute::kGroup},
-    {"id", Attribute::kId},
-    {"interpolate", Attribute::kInterpolate},
-    {"invariant", Attribute::kInvariant},
-    {"location", Attribute::kLocation},
-    {"must_use", Attribute::kMustUse},
-    {"size", Attribute::kSize},
-    {"vertex", Attribute::kVertex},
-    {"workgroup_size", Attribute::kWorkgroupSize},
+    {"align", Attribute::kAlign},           {"binding", Attribute::kBinding},
+    {"builtin", Attribute::kBuiltin},       {"compute", Attribute::kCompute},
+    {"diagnostic", Attribute::kDiagnostic}, {"fragment", Attribute::kFragment},
+    {"group", Attribute::kGroup},           {"id", Attribute::kId},
+    {"index", Attribute::kIndex},           {"interpolate", Attribute::kInterpolate},
+    {"invariant", Attribute::kInvariant},   {"location", Attribute::kLocation},
+    {"must_use", Attribute::kMustUse},      {"size", Attribute::kSize},
+    {"vertex", Attribute::kVertex},         {"workgroup_size", Attribute::kWorkgroupSize},
 };
 
 static constexpr Case kInvalidCases[] = {
@@ -85,27 +78,30 @@
     {"d", Attribute::kUndefined},
     {"i", Attribute::kUndefined},
     {"OVd", Attribute::kUndefined},
-    {"inyerpolae", Attribute::kUndefined},
-    {"rrnterpolll77Ge", Attribute::kUndefined},
-    {"inte4pol00te", Attribute::kUndefined},
-    {"inoornt", Attribute::kUndefined},
-    {"inzzriat", Attribute::kUndefined},
-    {"n11pariiin", Attribute::kUndefined},
-    {"XXocation", Attribute::kUndefined},
-    {"lIIc9955nnon", Attribute::kUndefined},
-    {"aaoHHatioYSS", Attribute::kUndefined},
-    {"mkksue", Attribute::kUndefined},
-    {"gjs_RRs", Attribute::kUndefined},
-    {"msb_se", Attribute::kUndefined},
-    {"jize", Attribute::kUndefined},
-    {"sze", Attribute::kUndefined},
-    {"qz", Attribute::kUndefined},
-    {"vNNtex", Attribute::kUndefined},
-    {"vevvx", Attribute::kUndefined},
-    {"veQQex", Attribute::kUndefined},
-    {"workgrrupffie", Attribute::kUndefined},
-    {"workgroup_sije", Attribute::kUndefined},
-    {"workgoupNNwsiz8", Attribute::kUndefined},
+    {"ndyx", Attribute::kUndefined},
+    {"n77rrldGx", Attribute::kUndefined},
+    {"inde40", Attribute::kUndefined},
+    {"itooolate", Attribute::kUndefined},
+    {"intezplate", Attribute::kUndefined},
+    {"ppnerii1olat", Attribute::kUndefined},
+    {"invarianXX", Attribute::kUndefined},
+    {"inv55ria99nII", Attribute::kUndefined},
+    {"irrvariaSSaHH", Attribute::kUndefined},
+    {"lkkcin", Attribute::kUndefined},
+    {"gjctRRo", Attribute::kUndefined},
+    {"lcbton", Attribute::kUndefined},
+    {"mustjuse", Attribute::kUndefined},
+    {"must_se", Attribute::kUndefined},
+    {"muquse", Attribute::kUndefined},
+    {"szNN", Attribute::kUndefined},
+    {"zvv", Attribute::kUndefined},
+    {"QQze", Attribute::kUndefined},
+    {"eterf", Attribute::kUndefined},
+    {"vertjx", Attribute::kUndefined},
+    {"v82wNNx", Attribute::kUndefined},
+    {"worgroup_size", Attribute::kUndefined},
+    {"workgrourr_size", Attribute::kUndefined},
+    {"workgroGp_size", Attribute::kUndefined},
 };
 
 using AttributeParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/builtin/extension.cc b/src/tint/builtin/extension.cc
index aca6220..a8d4567 100644
--- a/src/tint/builtin/extension.cc
+++ b/src/tint/builtin/extension.cc
@@ -40,6 +40,9 @@
     if (str == "chromium_experimental_push_constant") {
         return Extension::kChromiumExperimentalPushConstant;
     }
+    if (str == "chromium_internal_dual_source_blending") {
+        return Extension::kChromiumInternalDualSourceBlending;
+    }
     if (str == "chromium_internal_relaxed_uniform_layout") {
         return Extension::kChromiumInternalRelaxedUniformLayout;
     }
@@ -61,6 +64,8 @@
             return out << "chromium_experimental_full_ptr_parameters";
         case Extension::kChromiumExperimentalPushConstant:
             return out << "chromium_experimental_push_constant";
+        case Extension::kChromiumInternalDualSourceBlending:
+            return out << "chromium_internal_dual_source_blending";
         case Extension::kChromiumInternalRelaxedUniformLayout:
             return out << "chromium_internal_relaxed_uniform_layout";
         case Extension::kF16:
diff --git a/src/tint/builtin/extension.h b/src/tint/builtin/extension.h
index 6beeda3..6883f4b 100644
--- a/src/tint/builtin/extension.h
+++ b/src/tint/builtin/extension.h
@@ -36,6 +36,7 @@
     kChromiumExperimentalDp4A,
     kChromiumExperimentalFullPtrParameters,
     kChromiumExperimentalPushConstant,
+    kChromiumInternalDualSourceBlending,
     kChromiumInternalRelaxedUniformLayout,
     kF16,
 };
@@ -51,9 +52,13 @@
 Extension ParseExtension(std::string_view str);
 
 constexpr const char* kExtensionStrings[] = {
-    "chromium_disable_uniformity_analysis",      "chromium_experimental_dp4a",
-    "chromium_experimental_full_ptr_parameters", "chromium_experimental_push_constant",
-    "chromium_internal_relaxed_uniform_layout",  "f16",
+    "chromium_disable_uniformity_analysis",
+    "chromium_experimental_dp4a",
+    "chromium_experimental_full_ptr_parameters",
+    "chromium_experimental_push_constant",
+    "chromium_internal_dual_source_blending",
+    "chromium_internal_relaxed_uniform_layout",
+    "f16",
 };
 
 // A unique vector of extensions
diff --git a/src/tint/builtin/extension_bench.cc b/src/tint/builtin/extension_bench.cc
index 7a50281..447bf55 100644
--- a/src/tint/builtin/extension_bench.cc
+++ b/src/tint/builtin/extension_bench.cc
@@ -59,20 +59,27 @@
         "chromium_exp9rimFntal_ush_constant",
         "chrmium_experimental_push_constant",
         "cOOromium_experiVeHtal_puh_conRRtant",
-        "chromium_internl_relaxyd_uniform_layout",
-        "chromnnum_internrr77_Gelaxell_uniform_layout",
-        "chromium_intern4l_relaxe00_uniform_layout",
+        "chromium_internay_dual_sorce_blending",
+        "chrnnmium_internal_duGrr_source_bllend77ng",
+        "chromiu4_inter00al_dual_source_blending",
+        "chromium_internal_dual_source_blending",
+        "chromium_intoornal_dua_sorce_bleding",
+        "cromium_nternal_dual_sourcezzblending",
+        "chiiomiu_internal_dppal_source11blendig",
+        "XXhromium_internal_relaxed_uniform_layout",
+        "chromium_99nternal_reIInaxed_un55form_layout",
+        "chromYuaaSSinternrrl_rHHlaxed_uniform_layout",
         "chromium_internal_relaxed_uniform_layout",
-        "chrmoom_internal_relaxed_uniform_lyout",
-        "chroium_internal_rlaxed_uniform_layzzut",
-        "chromium_internaii_r11axed_uppifor_layout",
-        "f1XX",
-        "55199II",
-        "frSSHHa",
+        "chroiHm_internal_rkklaxd_uniform_layou",
+        "chromium_internl_relaxedguniRRorm_lajou",
+        "chromium_inernal_relaxedbuniorm_layout",
+        "fj6",
+        "f6",
+        "q",
         "f16",
-        "U",
-        "jV3",
-        "",
+        "fNN",
+        "fv",
+        "Q16",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
diff --git a/src/tint/builtin/extension_test.cc b/src/tint/builtin/extension_test.cc
index 9ef8683..cc13b13 100644
--- a/src/tint/builtin/extension_test.cc
+++ b/src/tint/builtin/extension_test.cc
@@ -48,6 +48,7 @@
     {"chromium_experimental_full_ptr_parameters",
      Extension::kChromiumExperimentalFullPtrParameters},
     {"chromium_experimental_push_constant", Extension::kChromiumExperimentalPushConstant},
+    {"chromium_internal_dual_source_blending", Extension::kChromiumInternalDualSourceBlending},
     {"chromium_internal_relaxed_uniform_layout", Extension::kChromiumInternalRelaxedUniformLayout},
     {"f16", Extension::kF16},
 };
@@ -65,12 +66,15 @@
     {"chvomium_experimental_push_constiint", Extension::kUndefined},
     {"chromiu8WWexperimental_push_constant", Extension::kUndefined},
     {"chromium_experiMental_push_costanxx", Extension::kUndefined},
-    {"chromium_internal_relaxed_unXform_layugg", Extension::kUndefined},
-    {"chromiuu_iVterna_relxed_unifXrm_layout", Extension::kUndefined},
-    {"chromium_internal_relaxed_uni3orm_layout", Extension::kUndefined},
-    {"fE6", Extension::kUndefined},
-    {"fPTT", Extension::kUndefined},
-    {"dxx6", Extension::kUndefined},
+    {"Xhromium_ggnternal_dual_sourceblending", Extension::kUndefined},
+    {"chromium_internludual_sorce_bVenXing", Extension::kUndefined},
+    {"chromium_internal_dual_source_b3ending", Extension::kUndefined},
+    {"chromium_internal_rElaxed_uniform_layout", Extension::kUndefined},
+    {"chromium_internalPPrTTlaed_uniform_layout", Extension::kUndefined},
+    {"chroddium_internxxl_relaxed_unform_layout", Extension::kUndefined},
+    {"4416", Extension::kUndefined},
+    {"fSVV6", Extension::kUndefined},
+    {"RR2", Extension::kUndefined},
 };
 
 using ExtensionParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/intrinsics.def b/src/tint/intrinsics.def
index 0d667ab..4943c1c 100644
--- a/src/tint/intrinsics.def
+++ b/src/tint/intrinsics.def
@@ -76,6 +76,8 @@
   chromium_experimental_full_ptr_parameters
   // A Chromium-specific extension that relaxes memory layout requirements for uniform storage.
   chromium_internal_relaxed_uniform_layout
+  // A Chromium-specific extension that enables dual source blending.
+  chromium_internal_dual_source_blending
 }
 
 // https://gpuweb.github.io/gpuweb/wgsl/#storage-class
@@ -260,6 +262,7 @@
   fragment
   group
   id
+  index
   interpolate
   invariant
   location
diff --git a/src/tint/program_builder.h b/src/tint/program_builder.h
index 4f4788f..f8866ac 100644
--- a/src/tint/program_builder.h
+++ b/src/tint/program_builder.h
@@ -50,6 +50,7 @@
 #include "src/tint/ast/if_statement.h"
 #include "src/tint/ast/increment_decrement_statement.h"
 #include "src/tint/ast/index_accessor_expression.h"
+#include "src/tint/ast/index_attribute.h"
 #include "src/tint/ast/int_literal_expression.h"
 #include "src/tint/ast/interpolate_attribute.h"
 #include "src/tint/ast/invariant_attribute.h"
@@ -3264,6 +3265,23 @@
         return create<ast::LocationAttribute>(source_, Expr(std::forward<EXPR>(location)));
     }
 
+    /// Creates an ast::IndexAttribute
+    /// @param source the source information
+    /// @param index the index value expression
+    /// @returns the index attribute pointer
+    template <typename EXPR>
+    const ast::IndexAttribute* Index(const Source& source, EXPR&& index) {
+        return create<ast::IndexAttribute>(source, Expr(std::forward<EXPR>(index)));
+    }
+
+    /// Creates an ast::IndexAttribute
+    /// @param index the index value expression
+    /// @returns the index attribute pointer
+    template <typename EXPR>
+    const ast::IndexAttribute* Index(EXPR&& index) {
+        return create<ast::IndexAttribute>(source_, Expr(std::forward<EXPR>(index)));
+    }
+
     /// Creates an ast::IdAttribute
     /// @param source the source information
     /// @param id the id value
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 3c63a02..621206f 100644
--- a/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
@@ -164,7 +164,7 @@
     // Error when unknown extension found
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), R"(1:8: expected extension
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
     auto program = p->program();
     auto& ast = program.AST();
     EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -178,7 +178,7 @@
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), R"(1:8: expected extension
 Did you mean 'f16'?
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
     auto program = p->program();
     auto& ast = program.AST();
     EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -226,7 +226,7 @@
         p->translation_unit();
         EXPECT_TRUE(p->has_error());
         EXPECT_EQ(p->error(), R"(1:8: expected extension
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
         auto program = p->program();
         auto& ast = program.AST();
         EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -237,7 +237,7 @@
         p->translation_unit();
         EXPECT_TRUE(p->has_error());
         EXPECT_EQ(p->error(), R"(1:8: expected extension
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
         auto program = p->program();
         auto& ast = program.AST();
         EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -249,7 +249,7 @@
         EXPECT_TRUE(p->has_error());
         EXPECT_EQ(p->error(), R"(1:8: expected extension
 Did you mean 'f16'?
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
         auto program = p->program();
         auto& ast = program.AST();
         EXPECT_EQ(ast.Enables().Length(), 0u);
diff --git a/src/tint/reader/wgsl/parser_impl_error_resync_test.cc b/src/tint/reader/wgsl/parser_impl_error_resync_test.cc
index f3e306a..9817bec 100644
--- a/src/tint/reader/wgsl/parser_impl_error_resync_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_error_resync_test.cc
@@ -54,7 +54,7 @@
      ^
 
 test.wgsl:4:2 error: expected attribute
-Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
+Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
 @_ fn -> {}
  ^
 )");
@@ -122,7 +122,7 @@
          ^^^^
 
 test.wgsl:7:6 error: expected attribute
-Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
+Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
     @- x : i32,
      ^
 )");
diff --git a/src/tint/reader/wgsl/parser_impl_function_attribute_list_test.cc b/src/tint/reader/wgsl/parser_impl_function_attribute_list_test.cc
index ae43a0f..52d7da1 100644
--- a/src/tint/reader/wgsl/parser_impl_function_attribute_list_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_function_attribute_list_test.cc
@@ -53,7 +53,7 @@
     EXPECT_TRUE(attrs.value.IsEmpty());
     EXPECT_EQ(p->error(), R"(1:2: expected attribute
 Did you mean 'invariant'?
-Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
+Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
 }
 
 }  // namespace
diff --git a/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc b/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc
index 1751e98..76c27e7 100644
--- a/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc
@@ -51,7 +51,7 @@
     EXPECT_TRUE(attrs.value.IsEmpty());
     EXPECT_EQ(p->error(), R"(1:2: expected attribute
 Did you mean 'invariant'?
-Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
+Possible values: 'align', 'binding', 'builtin', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
 }
 
 }  // namespace
diff --git a/src/tint/resolver/dependency_graph.cc b/src/tint/resolver/dependency_graph.cc
index 504e8ac..84932ed 100644
--- a/src/tint/resolver/dependency_graph.cc
+++ b/src/tint/resolver/dependency_graph.cc
@@ -34,6 +34,7 @@
 #include "src/tint/ast/identifier.h"
 #include "src/tint/ast/if_statement.h"
 #include "src/tint/ast/increment_decrement_statement.h"
+#include "src/tint/ast/index_attribute.h"
 #include "src/tint/ast/internal_attribute.h"
 #include "src/tint/ast/interpolate_attribute.h"
 #include "src/tint/ast/invariant_attribute.h"
@@ -390,6 +391,10 @@
                 TraverseExpression(id->expr);
                 return true;
             },
+            [&](const ast::IndexAttribute* index) {
+                TraverseExpression(index->expr);
+                return true;
+            },
             [&](const ast::InterpolateAttribute* interpolate) {
                 TraverseExpression(interpolate->type);
                 TraverseExpression(interpolate->sampling);
diff --git a/src/tint/resolver/dual_source_blending_extension_test.cc b/src/tint/resolver/dual_source_blending_extension_test.cc
new file mode 100644
index 0000000..ebaf61c
--- /dev/null
+++ b/src/tint/resolver/dual_source_blending_extension_test.cc
@@ -0,0 +1,88 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/resolver/resolver.h"
+#include "src/tint/resolver/resolver_test_helper.h"
+
+#include "gmock/gmock.h"
+
+using namespace tint::number_suffixes;  // NOLINT
+
+namespace tint::resolver {
+namespace {
+
+using DualSourceBlendingExtensionTest = ResolverTest;
+
+// Using the @index attribute without chromium_internal_dual_source_blending enabled should fail.
+TEST_F(DualSourceBlendingExtensionTest, UseIndexAttribWithoutExtensionError) {
+    Structure("Output", utils::Vector{
+                            Member("a", ty.vec4<f32>(),
+                                   utils::Vector{Location(0_a), Index(Source{{12, 34}}, 0_a)}),
+                        });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              "12:34 error: use of '@index' attribute requires enabling extension "
+              "'chromium_internal_dual_source_blending'");
+}
+
+TEST_F(DualSourceBlendingExtensionTest, IndexF32Error) {
+    Enable(builtin::Extension::kChromiumInternalDualSourceBlending);
+
+    Structure("Output", utils::Vector{
+                            Member(Source{{12, 34}}, "a", ty.vec4<f32>(),
+                                   utils::Vector{Location(0_a), Index(Source{{12, 34}}, 0_f)}),
+                        });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: @location must be an i32 or u32 value");
+}
+
+TEST_F(DualSourceBlendingExtensionTest, IndexFloatValueError) {
+    Enable(builtin::Extension::kChromiumInternalDualSourceBlending);
+
+    Structure("Output", utils::Vector{
+                            Member(Source{{12, 34}}, "a", ty.vec4<f32>(),
+                                   utils::Vector{Location(0_a), Index(Source{{12, 34}}, 1.0_a)}),
+                        });
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: @location must be an i32 or u32 value");
+}
+
+TEST_F(DualSourceBlendingExtensionTest, IndexNegativeValue) {
+    Enable(builtin::Extension::kChromiumInternalDualSourceBlending);
+
+    Structure("Output", utils::Vector{
+                            Member(Source{{12, 34}}, "a", ty.vec4<f32>(),
+                                   utils::Vector{Location(0_a), Index(Source{{12, 34}}, -1_a)}),
+                        });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: @index value must be zero or one");
+}
+
+TEST_F(DualSourceBlendingExtensionTest, IndexValueAboveOne) {
+    Enable(builtin::Extension::kChromiumInternalDualSourceBlending);
+
+    Structure("Output", utils::Vector{
+                            Member(Source{{12, 34}}, "a", ty.vec4<f32>(),
+                                   utils::Vector{Location(0_a), Index(Source{{12, 34}}, 2_a)}),
+                        });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: @index value must be zero or one");
+}
+
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 74a462a..51bf27c 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -3570,6 +3570,30 @@
     return static_cast<uint32_t>(value);
 }
 
+utils::Result<uint32_t> Resolver::IndexAttribute(const ast::IndexAttribute* attr) {
+    ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@index value"};
+    TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
+    auto* materialized = Materialize(ValueExpression(attr->expr));
+    if (!materialized) {
+        return utils::Failure;
+    }
+
+    if (!materialized->Type()->IsAnyOf<type::I32, type::U32>()) {
+        AddError("@location must be an i32 or u32 value", attr->source);
+        return utils::Failure;
+    }
+
+    auto const_value = materialized->ConstantValue();
+    auto value = const_value->ValueAs<AInt>();
+    if (value != 0 && value != 1) {
+        AddError("@index value must be zero or one", attr->source);
+        return utils::Failure;
+    }
+
+    return static_cast<uint32_t>(value);
+}
+
 utils::Result<uint32_t> Resolver::BindingAttribute(const ast::BindingAttribute* attr) {
     ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@binding"};
     TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
@@ -4142,6 +4166,14 @@
                     attributes.location = value.Get();
                     return true;
                 },
+                [&](const ast::IndexAttribute* attr) {
+                    auto value = IndexAttribute(attr);
+                    if (!value) {
+                        return false;
+                    }
+                    attributes.index = value.Get();
+                    return true;
+                },
                 [&](const ast::BuiltinAttribute* attr) {
                     auto value = BuiltinAttribute(attr);
                     if (!value) {
diff --git a/src/tint/resolver/resolver.h b/src/tint/resolver/resolver.h
index 5d13d25..46ac83f 100644
--- a/src/tint/resolver/resolver.h
+++ b/src/tint/resolver/resolver.h
@@ -320,6 +320,10 @@
     /// @returns the location value on success.
     utils::Result<uint32_t> LocationAttribute(const ast::LocationAttribute* attr);
 
+    /// Resolves the `@index` attribute @p attr
+    /// @returns the index value on success.
+    utils::Result<uint32_t> IndexAttribute(const ast::IndexAttribute* attr);
+
     /// Resolves the `@binding` attribute @p attr
     /// @returns the binding value on success.
     utils::Result<uint32_t> BindingAttribute(const ast::BindingAttribute* attr);
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index eb009c2..17b75e1 100644
--- a/src/tint/resolver/validator.cc
+++ b/src/tint/resolver/validator.cc
@@ -29,6 +29,7 @@
 #include "src/tint/ast/for_loop_statement.h"
 #include "src/tint/ast/id_attribute.h"
 #include "src/tint/ast/if_statement.h"
+#include "src/tint/ast/index_attribute.h"
 #include "src/tint/ast/internal_attribute.h"
 #include "src/tint/ast/interpolate_attribute.h"
 #include "src/tint/ast/loop_statement.h"
@@ -1115,6 +1116,8 @@
                                        is_input)) {
                     return false;
                 }
+            } else if (auto* index_attr = attr->As<ast::IndexAttribute>()) {
+                return IndexAttribute(index_attr);
             } else if (auto* interpolate = attr->As<ast::InterpolateAttribute>()) {
                 if (decl->PipelineStage() == ast::PipelineStage::kCompute) {
                     is_invalid_compute_shader_attribute = true;
@@ -2113,6 +2116,7 @@
                     }
                     return true;
                 },
+                [&](const ast::IndexAttribute* index) { return IndexAttribute(index); },
                 [&](const ast::BuiltinAttribute* builtin_attr) {
                     if (!BuiltinAttribute(builtin_attr, member->Type(), stage,
                                           /* is_input */ false)) {
@@ -2197,6 +2201,18 @@
     return true;
 }
 
+bool Validator::IndexAttribute(const ast::IndexAttribute* index_attr) const {
+    if (!enabled_extensions_.Contains(builtin::Extension::kChromiumInternalDualSourceBlending)) {
+        AddError(
+            "use of '@index' attribute requires enabling extension "
+            "'chromium_internal_dual_source_blending'",
+            index_attr->source);
+        return false;
+    }
+
+    return false;
+}
+
 bool Validator::Return(const ast::ReturnStatement* ret,
                        const type::Type* func_type,
                        const type::Type* ret_type,
diff --git a/src/tint/resolver/validator.h b/src/tint/resolver/validator.h
index 999bc49..19ceb5e 100644
--- a/src/tint/resolver/validator.h
+++ b/src/tint/resolver/validator.h
@@ -329,6 +329,11 @@
                            const Source& source,
                            const bool is_input = false) const;
 
+    /// Validates a index attribute
+    /// @param index_attr the index attribute to validate
+    /// @returns true on success, false otherwise.
+    bool IndexAttribute(const ast::IndexAttribute* index_attr) const;
+
     /// Validates a loop statement
     /// @param stmt the loop statement
     /// @returns true on success, false otherwise.
diff --git a/src/tint/type/struct.h b/src/tint/type/struct.h
index 720beca..907485c 100644
--- a/src/tint/type/struct.h
+++ b/src/tint/type/struct.h
@@ -185,6 +185,8 @@
 struct StructMemberAttributes {
     /// The value of a `@location` attribute
     std::optional<uint32_t> location;
+    /// The value of a `@index` attribute
+    std::optional<uint32_t> index;
     /// The value of a `@builtin` attribute
     std::optional<builtin::BuiltinValue> builtin;
     /// The values of a `@interpolate` attribute