[tint][resolver] Fix dual source blending validation

The check for an entry point using a non-zero @location and non-zero
@index would only fire if the non-zero index came after the @location.

This now only validates on the entry point (as opposed to a standalone
struct), but given that this is non-spec'd this seems fine.

Rework the validation, and change the diagnostic message to be clearer
about what you did wrong.

Change-Id: I3e35b316814cb149c1f8f77f59e236463d87b2aa
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/160082
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: James Price <jrprice@google.com>
diff --git a/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc b/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc
index 74451cd..9ea0b05 100644
--- a/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc
+++ b/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc
@@ -107,7 +107,7 @@
     Structure("Output", Vector{
                             Member("a", ty.vec4<f32>(), Vector{Location(0_a), Index(0_a)}),
                             Member(Source{{12, 34}}, "b", ty.vec4<f32>(),
-                                   Vector{Location(0_a), Index(Source{{12, 34}}, 0_a)}),
+                                   Vector{Location(Source{{12, 34}}, 0_a), Index(0_a)}),
                         });
 
     EXPECT_FALSE(r()->Resolve());
@@ -198,22 +198,40 @@
 };
 
 // Rendering to multiple render targets while using dual source blending should fail.
-TEST_P(DualSourceBlendingExtensionTestWithParams, MultipleRenderTargetsNotAllowed) {
+TEST_P(DualSourceBlendingExtensionTestWithParams,
+       MultipleRenderTargetsNotAllowed_IndexThenNonZeroLocation) {
     Structure("S",
               Vector{
                   Member("a", ty.vec4<f32>(), Vector{Location(0_a), Index(0_a)}),
-                  Member("b", ty.vec4<f32>(), Vector{Location(0_a), Index(1_a)}),
-                  Member("c", ty.vec4<f32>(), Vector{Location(Source{{12, 34}}, AInt(GetParam()))}),
+                  Member("b", ty.vec4<f32>(), Vector{Location(0_a), Index(Source{{1, 2}}, 1_a)}),
+                  Member("c", ty.vec4<f32>(), Vector{Location(Source{{3, 4}}, AInt(GetParam()))}),
               });
     Func("F", Empty, ty("S"), Vector{Return(Call("S"))},
          Vector{Stage(ast::PipelineStage::kFragment)});
 
     EXPECT_FALSE(r()->Resolve());
-    StringStream err;
-    err << "12:34 error: Multiple render targets are not allowed when using dual source blending. "
-           "The output @location("
-        << GetParam() << ") is not allowed as a render target.";
-    EXPECT_EQ(r()->error(), err.str());
+    EXPECT_EQ(r()->error(),
+              R"(1:2 error: pipeline cannot use both non-zero @index and non-zero @location
+3:4 note: non-zero @location declared here
+note: while analyzing entry point 'F')");
+}
+
+TEST_P(DualSourceBlendingExtensionTestWithParams,
+       MultipleRenderTargetsNotAllowed_NonZeroLocationThenIndex) {
+    Structure("S",
+              Vector{
+                  Member("a", ty.vec4<f32>(), Vector{Location(Source{{1, 2}}, AInt(GetParam()))}),
+                  Member("b", ty.vec4<f32>(), Vector{Location(0_a), Index(0_a)}),
+                  Member("c", ty.vec4<f32>(), Vector{Location(0_a), Index(Source{{3, 4}}, 1_a)}),
+              });
+    Func(Source{{5, 6}}, "F", Empty, ty("S"), Vector{Return(Call("S"))},
+         Vector{Stage(ast::PipelineStage::kFragment)});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              R"(3:4 error: pipeline cannot use both non-zero @index and non-zero @location
+1:2 note: non-zero @location declared here
+5:6 note: while analyzing entry point 'F')");
 }
 
 INSTANTIATE_TEST_SUITE_P(DualSourceBlendingExtensionTests,
diff --git a/src/tint/lang/wgsl/resolver/validator.cc b/src/tint/lang/wgsl/resolver/validator.cc
index 1a2cff8..67ba42d 100644
--- a/src/tint/lang/wgsl/resolver/validator.cc
+++ b/src/tint/lang/wgsl/resolver/validator.cc
@@ -1113,7 +1113,9 @@
     // TODO(jrprice): This state could be stored in sem::Function instead, and then passed to
     // sem::Function since it would be useful there too.
     Hashset<core::BuiltinValue, 4> builtins;
-    Hashset<std::pair<uint32_t, uint32_t>, 8> locationsAndIndexes;
+    Hashset<std::pair<uint32_t, uint32_t>, 8> locations_and_indices;
+    const ast::LocationAttribute* first_nonzero_location = nullptr;
+    const ast::IndexAttribute* first_nonzero_index = nullptr;
     enum class ParamOrRetType {
         kParameter,
         kReturnType,
@@ -1181,15 +1183,8 @@
                     return LocationAttribute(loc_attr, ty, stage, source);
                 },
                 [&](const ast::IndexAttribute* index_attr) {
-                    bool is_input = param_or_ret == ParamOrRetType::kParameter;
                     index_attribute = index_attr;
-
-                    if (TINT_UNLIKELY(!index.has_value())) {
-                        TINT_ICE() << "@index has no value";
-                        return false;
-                    }
-
-                    return IndexAttribute(index_attr, stage, is_input);
+                    return IndexAttribute(index_attr, stage);
                 },
                 [&](const ast::InterpolateAttribute* interpolate) {
                     interpolate_attribute = interpolate;
@@ -1254,20 +1249,28 @@
             }
 
             if (location_attribute) {
-                uint32_t idx = 0xffffffff;
-                if (index_attribute) {
-                    idx = index.value();
+                if (!first_nonzero_location && location > 0u) {
+                    first_nonzero_location = location_attribute;
+                }
+                if (!first_nonzero_index && index > 0u) {
+                    first_nonzero_index = index_attribute;
+                }
+                if (first_nonzero_location && first_nonzero_index) {
+                    AddError("pipeline cannot use both non-zero @index and non-zero @location",
+                             first_nonzero_index->source);
+                    AddNote("non-zero @location declared here", first_nonzero_location->source);
+                    return false;
                 }
 
-                std::pair<uint32_t, uint32_t> locationAndIndex(location.value(), idx);
-                if (!locationsAndIndexes.Add(locationAndIndex)) {
+                std::pair<uint32_t, uint32_t> location_and_index(location.value(),
+                                                                 index.value_or(0));
+                if (!locations_and_indices.Add(location_and_index)) {
                     StringStream err;
-                    if (!index_attribute) {
-                        err << "@location(" << location.value() << ") appears multiple times";
-                    } else {
-                        err << "@location(" << location.value() << ") @index(" << index.value()
-                            << ") appears multiple times";
+                    err << "@location(" << location.value() << ") ";
+                    if (index_attribute) {
+                        err << "@index(" << index.value() << ") ";
                     }
+                    err << "appears multiple times";
                     AddError(err.str(), location_attribute->source);
                     return false;
                 }
@@ -1330,10 +1333,10 @@
 
     for (auto* param : func->Parameters()) {
         auto* param_decl = param->Declaration();
+        auto& attrs = param->Attributes();
         if (!validate_entry_point_attributes(param_decl->attributes, param->Type(),
                                              param_decl->source, ParamOrRetType::kParameter,
-                                             param->Attributes().location,
-                                             param->Attributes().index)) {
+                                             attrs.location, attrs.index)) {
             return false;
         }
     }
@@ -1341,7 +1344,7 @@
     // Clear IO sets after parameter validation. Builtin and location attributes in return types
     // should be validated independently from those used in parameters.
     builtins.Clear();
-    locationsAndIndexes.Clear();
+    locations_and_indices.Clear();
 
     if (!func->ReturnType()->Is<core::type::Void>()) {
         if (!validate_entry_point_attributes(decl->return_type_attributes, func->ReturnType(),
@@ -2150,8 +2153,7 @@
         return false;
     }
 
-    auto has_index = false;
-    Hashset<std::pair<uint32_t, uint32_t>, 8> locationsAndIndexes;
+    Hashset<std::pair<uint32_t, uint32_t>, 8> locations_and_indices;
     for (auto* member : str->Members()) {
         if (auto* r = member->Type()->As<sem::Array>()) {
             if (r->Count()->Is<core::type::RuntimeArrayCount>()) {
@@ -2193,7 +2195,6 @@
                 },
                 [&](const ast::IndexAttribute* index) {
                     index_attribute = index;
-                    has_index = true;
                     return IndexAttribute(index, stage);
                 },
                 [&](const ast::BuiltinAttribute* builtin_attr) {
@@ -2250,32 +2251,18 @@
 
         // Ensure all locations and index pairs are unique
         if (location_attribute) {
-            uint32_t index = 0xffffffff;
-            if (index_attribute) {
-                index = member->Attributes().index.value();
-            }
             uint32_t location = member->Attributes().location.value();
-            if (has_index && location != 0) {
-                StringStream err;
-                err << "Multiple render targets are not allowed when using dual source blending. "
-                       "The output @location("
-                    << location << ") is not allowed as a render target.";
-                AddError(err.str(), location_attribute->source);
-                return false;
-            }
+            uint32_t index = member->Attributes().index.value_or(0);
 
-            std::pair<uint32_t, uint32_t> locationAndIndex(location, index);
-            if (!locationsAndIndexes.Add(locationAndIndex)) {
+            std::pair<uint32_t, uint32_t> location_and_index(location, index);
+            if (!locations_and_indices.Add(location_and_index)) {
                 StringStream err;
-                if (!index_attribute) {
-                    err << "@location(" << location << ") appears multiple times";
-                    AddError(err.str(), location_attribute->source);
-                } else {
-                    err << "@location(" << location << ") @index(" << index
-                        << ") appears multiple times";
-                    AddError(err.str(), index_attribute->source);
+                err << "@location(" << location << ") ";
+                if (index_attribute) {
+                    err << "@index(" << index << ") ";
                 }
-
+                err << "appears multiple times";
+                AddError(err.str(), location_attribute->source);
                 return false;
             }
         }
diff --git a/src/tint/lang/wgsl/resolver/validator.h b/src/tint/lang/wgsl/resolver/validator.h
index e78512a..03466ee 100644
--- a/src/tint/lang/wgsl/resolver/validator.h
+++ b/src/tint/lang/wgsl/resolver/validator.h
@@ -344,7 +344,7 @@
     /// @param attr the attribute to validate
     /// @param type the variable type
     /// @param stage the current pipeline stage
-    /// @param source the source of the attribute
+    /// @param source the source of declaration using the attribute
     /// @returns true on success, false otherwise.
     bool LocationAttribute(const ast::LocationAttribute* attr,
                            const core::type::Type* type,
@@ -359,7 +359,7 @@
     /// @returns true on success, false otherwise.
     bool IndexAttribute(const ast::IndexAttribute* index_attr,
                         ast::PipelineStage stage,
-                        const std::optional<bool> is_input = {}) const;
+                        const std::optional<bool> is_input = std::nullopt) const;
 
     /// Validates a loop statement
     /// @param stmt the loop statement