[tint][ir][val] Handle construct with undefined args or results

- Implements `Unused` value in core IR
- Uses `Unused` in MSL backend to represent struct values that are not
  used in the ModuleScopeVars to avoid validator failure

Fixes: 356878395
Fixes: 356896466
Change-Id: I6d0580b8eebf1fed12e75a001a4273bef74e6f7c
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/201095
Commit-Queue: dan sinclair <dsinclair@chromium.org>
Auto-Submit: Ryan Harrison <rharrison@chromium.org>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
diff --git a/src/tint/lang/core/ir/validator.cc b/src/tint/lang/core/ir/validator.cc
index d2d6157..aebbe9c 100644
--- a/src/tint/lang/core/ir/validator.cc
+++ b/src/tint/lang/core/ir/validator.cc
@@ -71,6 +71,7 @@
 #include "src/tint/lang/core/ir/terminate_invocation.h"
 #include "src/tint/lang/core/ir/unary.h"
 #include "src/tint/lang/core/ir/unreachable.h"
+#include "src/tint/lang/core/ir/unused.h"
 #include "src/tint/lang/core/ir/user_call.h"
 #include "src/tint/lang/core/ir/var.h"
 #include "src/tint/lang/core/type/bool.h"
@@ -271,12 +272,12 @@
     /// @returns true if the result is not null
     bool CheckResult(const Instruction* inst, size_t idx);
 
-    /// Checks the number of results for @p inst are exactly equal to @p count and that none of
-    /// them are null. Also checks that the types for the results are not null
+    /// Checks the results (and their types) for @p inst are not null. If count is specified then
+    /// number of results is checked to be exact.
     /// @param inst the instruction
     /// @param count the number of results to check
     /// @returns true if the results count is as expected and none are null
-    bool CheckResults(const ir::Instruction* inst, size_t count);
+    bool CheckResults(const ir::Instruction* inst, std::optional<size_t> count);
 
     /// Checks the given operand is not null and its type is not null
     /// @param inst the instruction
@@ -295,12 +296,12 @@
                        size_t min_count,
                        std::optional<size_t> max_count);
 
-    /// Checks the number of operands for @p inst are exactly equal to @p count and that none of
-    /// them are null. Also checks that the types for the operands are not null
+    /// Checks the operands (and their types) for @p inst are not null. If count is specified then
+    /// number of operands is checked to be exact.
     /// @param inst the instruction
     /// @param count the number of operands to check
     /// @returns true if the operands count is as expected and none are null
-    bool CheckOperands(const ir::Instruction* inst, size_t count);
+    bool CheckOperands(const ir::Instruction* inst, std::optional<size_t> count);
 
     /// Checks the number of results for @p inst are exactly equal to @p num_results and the number
     /// of operands is correctly. Both results and operands are confirmed to be non-null.
@@ -325,6 +326,12 @@
                                  size_t num_results,
                                  size_t num_operands);
 
+    /// Checks that the results and operands (and their types) for @p inst are not null.
+    /// Note: Does not check the number of results and operands.
+    /// @param inst the instruction
+    /// @returns true if the results and operands are not null
+    bool CheckResultsAndOperands(const ir::Instruction* inst);
+
     /// Checks the given operand is not null
     /// @param inst the instruction
     /// @param operand the operand
@@ -774,15 +781,17 @@
     return true;
 }
 
-bool Validator::CheckResults(const ir::Instruction* inst, size_t count) {
-    if (TINT_UNLIKELY(inst->Results().Length() != count)) {
-        AddError(inst) << "expected exactly " << count << " results, got "
-                       << inst->Results().Length();
-        return false;
+bool Validator::CheckResults(const ir::Instruction* inst, std::optional<size_t> count = {}) {
+    if (count.has_value()) {
+        if (TINT_UNLIKELY(inst->Results().Length() != count.value())) {
+            AddError(inst) << "expected exactly " << count.value() << " results, got "
+                           << inst->Results().Length();
+            return false;
+        }
     }
 
     bool passed = true;
-    for (size_t i = 0; i < count; i++) {
+    for (size_t i = 0; i < inst->Results().Length(); i++) {
         if (TINT_UNLIKELY(!CheckResult(inst, i))) {
             passed = false;
         }
@@ -797,6 +806,12 @@
         return false;
     }
 
+    // ir::Unused is a internal value used by some transforms to track unused entries, and is
+    // removed as part of generating an output shader.
+    if (TINT_UNLIKELY(operand->Is<ir::Unused>())) {
+        return true;
+    }
+
     // ir::Function does not have a meaningful type, so does not override the default Type()
     // behaviour.
     if (TINT_UNLIKELY(!operand->Is<ir::Function>() && operand->Type() == nullptr)) {
@@ -836,15 +851,17 @@
     return passed;
 }
 
-bool Validator::CheckOperands(const ir::Instruction* inst, size_t count) {
-    if (TINT_UNLIKELY(inst->Operands().Length() != count)) {
-        AddError(inst) << "expected exactly " << count << " operands, got "
-                       << inst->Operands().Length();
-        return false;
+bool Validator::CheckOperands(const ir::Instruction* inst, std::optional<size_t> count = {}) {
+    if (count.has_value()) {
+        if (TINT_UNLIKELY(inst->Operands().Length() != count.value())) {
+            AddError(inst) << "expected exactly " << count.value() << " operands, got "
+                           << inst->Operands().Length();
+            return false;
+        }
     }
 
     bool passed = true;
-    for (size_t i = 0; i < count; i++) {
+    for (size_t i = 0; i < inst->Operands().Length(); i++) {
         if (TINT_UNLIKELY(!CheckOperand(inst, i))) {
             passed = false;
         }
@@ -871,6 +888,13 @@
     return results_passed && operands_passed;
 }
 
+bool Validator::CheckResultsAndOperands(const ir::Instruction* inst) {
+    // Intentionally avoiding short-circuiting here
+    bool results_passed = CheckResults(inst);
+    bool operands_passed = CheckOperands(inst);
+    return results_passed && operands_passed;
+}
+
 // TODO(353498500): Remove this function once it is no longer used.
 void Validator::CheckOperandNotNull(const Instruction* inst, const ir::Value* operand, size_t idx) {
     if (operand == nullptr) {
@@ -1162,7 +1186,7 @@
             AddError(inst, i) << "operand missing usage";
         } else if (auto fn = op->As<Function>(); fn && !all_functions_.Contains(fn)) {
             AddError(inst, i) << NameOf(op) << " is not part of the module";
-        } else if (!op->Is<Constant>() && !scope_stack_.Contains(op)) {
+        } else if (!op->Is<ir::Unused>() && !op->Is<Constant>() && !scope_stack_.Contains(op)) {
             AddError(inst, i) << NameOf(op) << " is not in scope";
             AddDeclarationNote(op);
         }
@@ -1336,6 +1360,10 @@
         return;
     }
 
+    if (!CheckResultsAndOperands(construct)) {
+        return;
+    }
+
     if (auto* str = As<type::Struct>(construct->Result(0)->Type())) {
         auto members = str->Members();
         if (args.Length() != str->Members().Length()) {
@@ -1345,7 +1373,10 @@
             return;
         }
         for (size_t i = 0; i < args.Length(); i++) {
-            if (args[i] && args[i]->Type() != members[i]->Type()) {
+            if (args[i]->Is<ir::Unused>()) {
+                continue;
+            }
+            if (args[i]->Type() != members[i]->Type()) {
                 AddError(construct, Construct::kArgsOperandOffset + i)
                     << "structure member " << i << " is of type "
                     << style::Type(members[i]->Type()->FriendlyName())