[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/BUILD.bazel b/src/tint/lang/core/ir/BUILD.bazel
index 575f7dd..99c054a 100644
--- a/src/tint/lang/core/ir/BUILD.bazel
+++ b/src/tint/lang/core/ir/BUILD.bazel
@@ -86,6 +86,7 @@
     "terminator.cc",
     "unary.cc",
     "unreachable.cc",
+    "unused.cc",
     "user_call.cc",
     "validator.cc",
     "value.cc",
@@ -141,6 +142,7 @@
     "traverse.h",
     "unary.h",
     "unreachable.h",
+    "unused.h",
     "user_call.h",
     "validator.h",
     "value.h",
diff --git a/src/tint/lang/core/ir/BUILD.cmake b/src/tint/lang/core/ir/BUILD.cmake
index 3e5bb13..7b9e40f 100644
--- a/src/tint/lang/core/ir/BUILD.cmake
+++ b/src/tint/lang/core/ir/BUILD.cmake
@@ -138,6 +138,8 @@
   lang/core/ir/unary.h
   lang/core/ir/unreachable.cc
   lang/core/ir/unreachable.h
+  lang/core/ir/unused.cc
+  lang/core/ir/unused.h
   lang/core/ir/user_call.cc
   lang/core/ir/user_call.h
   lang/core/ir/validator.cc
diff --git a/src/tint/lang/core/ir/BUILD.gn b/src/tint/lang/core/ir/BUILD.gn
index 87e0186..85e5573 100644
--- a/src/tint/lang/core/ir/BUILD.gn
+++ b/src/tint/lang/core/ir/BUILD.gn
@@ -140,6 +140,8 @@
     "unary.h",
     "unreachable.cc",
     "unreachable.h",
+    "unused.cc",
+    "unused.h",
     "user_call.cc",
     "user_call.h",
     "validator.cc",
diff --git a/src/tint/lang/core/ir/builder.cc b/src/tint/lang/core/ir/builder.cc
index a1fb84a..5cad9c1 100644
--- a/src/tint/lang/core/ir/builder.cc
+++ b/src/tint/lang/core/ir/builder.cc
@@ -139,6 +139,10 @@
     return Append(ir.allocators.instructions.Create<ir::Unreachable>(ir.NextInstructionId()));
 }
 
+ir::Unused* Builder::Unused() {
+    return ir.allocators.values.Create<ir::Unused>();
+}
+
 const core::type::Type* Builder::VectorPtrElementType(const core::type::Type* type) {
     auto* vec_ptr_ty = type->As<core::type::Pointer>();
     TINT_ASSERT(vec_ptr_ty);
diff --git a/src/tint/lang/core/ir/builder.h b/src/tint/lang/core/ir/builder.h
index 0787547..dd2d301 100644
--- a/src/tint/lang/core/ir/builder.h
+++ b/src/tint/lang/core/ir/builder.h
@@ -66,6 +66,7 @@
 #include "src/tint/lang/core/ir/swizzle.h"
 #include "src/tint/lang/core/ir/terminate_invocation.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/value.h"  // IWYU pragma: export
 #include "src/tint/lang/core/ir/var.h"
@@ -1654,6 +1655,10 @@
     /// @returns the instruction
     ir::Unreachable* Unreachable();
 
+    /// Creates an unused instruction
+    /// @returns the instruction
+    ir::Unused* Unused();
+
     /// Creates a new runtime value
     /// @param type the return type
     /// @returns the value
diff --git a/src/tint/lang/core/ir/disassembler.cc b/src/tint/lang/core/ir/disassembler.cc
index ed4fb2f..f72a756 100644
--- a/src/tint/lang/core/ir/disassembler.cc
+++ b/src/tint/lang/core/ir/disassembler.cc
@@ -60,6 +60,7 @@
 #include "src/tint/lang/core/ir/swizzle.h"
 #include "src/tint/lang/core/ir/terminate_invocation.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/struct.h"
@@ -446,6 +447,7 @@
                 };
             emit(constant->Value());
         },
+        [&](const tint::core::ir::Unused*) { out_ << StyleLiteral("unused"); },
         [&](Default) { out_ << NameOf(val); });
 }
 
diff --git a/src/tint/lang/core/ir/unused.cc b/src/tint/lang/core/ir/unused.cc
new file mode 100644
index 0000000..167510e
--- /dev/null
+++ b/src/tint/lang/core/ir/unused.cc
@@ -0,0 +1,45 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/unused.h"
+
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::core::ir::Unused);
+
+namespace tint::core::ir {
+
+Unused::Unused() = default;
+
+Unused::~Unused() = default;
+
+Unused* Unused::Clone(CloneContext& ctx) {
+    return ctx.ir.allocators.values.Create<Unused>();
+}
+
+}  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/unused.h b/src/tint/lang/core/ir/unused.h
new file mode 100644
index 0000000..8b33549
--- /dev/null
+++ b/src/tint/lang/core/ir/unused.h
@@ -0,0 +1,47 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_CORE_IR_UNUSED_H_
+#define SRC_TINT_LANG_CORE_IR_UNUSED_H_
+
+#include "src/tint/lang/core/ir/value.h"
+
+namespace tint::core::ir {
+
+/// Unused value in the IR.
+class Unused : public Castable<Unused, Value> {
+  public:
+    Unused();
+    ~Unused() override;
+
+    /// @copydoc Value::Clone()
+    Unused* Clone(CloneContext& ctx) override;
+};
+
+}  // namespace tint::core::ir
+
+#endif  // SRC_TINT_LANG_CORE_IR_UNUSED_H_
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())
diff --git a/src/tint/lang/core/ir/validator_test.cc b/src/tint/lang/core/ir/validator_test.cc
index 5fb3314..43f6d90 100644
--- a/src/tint/lang/core/ir/validator_test.cc
+++ b/src/tint/lang/core/ir/validator_test.cc
@@ -933,6 +933,22 @@
     ASSERT_EQ(res, Success) << res.Failure();
 }
 
+TEST_F(IR_ValidatorTest, Construct_Struct_UnusedArgs) {
+    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                              {mod.symbols.New("a"), ty.i32()},
+                                                              {mod.symbols.New("b"), ty.u32()},
+                                                          });
+
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        b.Construct(str_ty, 1_i, b.Unused());
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success) << res.Failure();
+}
+
 TEST_F(IR_ValidatorTest, Construct_Struct_NotEnoughArgs) {
     auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
                                                               {mod.symbols.New("a"), ty.i32()},
@@ -1048,6 +1064,91 @@
 )");
 }
 
+TEST_F(IR_ValidatorTest, Construct_NullArg) {
+    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                              {mod.symbols.New("a"), ty.i32()},
+                                                              {mod.symbols.New("b"), ty.u32()},
+                                                          });
+
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        b.Construct(str_ty, 1_i, nullptr);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:33 error: construct: operand is undefined
+    %2:MyStruct = construct 1i, undef
+                                ^^^^^
+
+:7:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+MyStruct = struct @align(4) {
+  a:i32 @offset(0)
+  b:u32 @offset(4)
+}
+
+%f = func():void {
+  $B1: {
+    %2:MyStruct = construct 1i, undef
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Construct_NullResult) {
+    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                              {mod.symbols.New("a"), ty.i32()},
+                                                              {mod.symbols.New("b"), ty.u32()},
+                                                          });
+
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* c = b.Construct(str_ty, 1_i, 2_u);
+        c->SetResults(Vector<ir::InstructionResult*, 1>{nullptr});
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:5 error: construct: result is undefined
+    undef = construct 1i, 2u
+    ^^^^^
+
+:7:3 note: in block
+  $B1: {
+  ^^^
+
+:8:5 error: construct: result is undefined
+    undef = construct 1i, 2u
+    ^^^^^
+
+:7:3 note: in block
+  $B1: {
+  ^^^
+
+note: # Disassembly
+MyStruct = struct @align(4) {
+  a:i32 @offset(0)
+  b:u32 @offset(4)
+}
+
+%f = func():void {
+  $B1: {
+    undef = construct 1i, 2u
+    ret
+  }
+}
+)");
+}
+
 TEST_F(IR_ValidatorTest, Block_NoTerminator) {
     b.Function("my_func", ty.void_());
 
@@ -5914,7 +6015,15 @@
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
     EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:5:15 error: store: operand type is undefined
+              R"(:4:5 error: construct: result type is undefined
+    %3:undef = construct 42u
+    ^^^^^^^^
+
+:2:3 note: in block
+  $B1: {
+  ^^^
+
+:5:15 error: store: operand type is undefined
     store %2, %3
               ^^
 
diff --git a/src/tint/lang/msl/writer/printer/printer.cc b/src/tint/lang/msl/writer/printer/printer.cc
index ad64f17..a36dd02 100644
--- a/src/tint/lang/msl/writer/printer/printer.cc
+++ b/src/tint/lang/msl/writer/printer/printer.cc
@@ -65,6 +65,7 @@
 #include "src/tint/lang/core/ir/swizzle.h"
 #include "src/tint/lang/core/ir/terminate_invocation.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/validator.h"
 #include "src/tint/lang/core/ir/var.h"
@@ -1049,8 +1050,8 @@
                 size_t i = 0;
                 bool needs_comma = false;
                 for (auto* arg : c->Args()) {
-                    if (arg == nullptr) {
-                        // Skip `undef` values.
+                    if (arg->Is<tint::core::ir::Unused>()) {
+                        // Skip `unused` values.
                         i++;
                         continue;
                     }
@@ -1058,7 +1059,7 @@
                         out << ", ";
                     }
                     // Emit field designators for structures so that we can skip padding members and
-                    // arguments that are `undef` values.
+                    // arguments that are `undef` or `unused` values.
                     auto name = struct_ty->Members()[i]->Name().Name();
                     out << "." << name << "=";
                     EmitAndTakeAddressIfNeeded(out, arg);
diff --git a/src/tint/lang/msl/writer/raise/module_scope_vars.cc b/src/tint/lang/msl/writer/raise/module_scope_vars.cc
index fea01b9..efbde47 100644
--- a/src/tint/lang/msl/writer/raise/module_scope_vars.cc
+++ b/src/tint/lang/msl/writer/raise/module_scope_vars.cc
@@ -183,8 +183,8 @@
             Vector<core::ir::Value*, 8> construct_args;
             for (auto var : module_vars) {
                 if (!referenced_vars.Contains(var)) {
-                    // The variable isn't used by this entry point, so set the member to undef.
-                    construct_args.Push(nullptr);
+                    // The variable isn't used by this entry point, so set the member to unused.
+                    construct_args.Push(b.Unused());
                     continue;
                 }
 
diff --git a/src/tint/lang/msl/writer/raise/module_scope_vars_test.cc b/src/tint/lang/msl/writer/raise/module_scope_vars_test.cc
index ede0e36..51b0aa8 100644
--- a/src/tint/lang/msl/writer/raise/module_scope_vars_test.cc
+++ b/src/tint/lang/msl/writer/raise/module_scope_vars_test.cc
@@ -1352,7 +1352,7 @@
 
 %main_a = @fragment func(%a:ptr<uniform, i32, read> [@binding_point(1, 2)], %b:ptr<storage, i32, read_write> [@binding_point(3, 4)]):void {
   $B1: {
-    %4:tint_module_vars_struct = construct %a, %b, undef
+    %4:tint_module_vars_struct = construct %a, %b, unused
     %tint_module_vars:tint_module_vars_struct = let %4
     %6:ptr<uniform, i32, read> = access %tint_module_vars, 0u
     %7:i32 = load %6
@@ -1367,7 +1367,7 @@
 %main_b = @fragment func(%a_1:ptr<uniform, i32, read> [@binding_point(1, 2)]):void {  # %a_1: 'a'
   $B2: {
     %c:ptr<private, i32, read_write> = var
-    %15:tint_module_vars_struct = construct %a_1, undef, %c
+    %15:tint_module_vars_struct = construct %a_1, unused, %c
     %tint_module_vars_1:tint_module_vars_struct = let %15  # %tint_module_vars_1: 'tint_module_vars'
     %17:ptr<uniform, i32, read> = access %tint_module_vars_1, 0u
     %18:i32 = load %17
@@ -1465,7 +1465,7 @@
 }
 %main_a = @fragment func(%a:ptr<uniform, i32, read> [@binding_point(1, 2)], %b:ptr<storage, i32, read_write> [@binding_point(3, 4)]):void {
   $B2: {
-    %8:tint_module_vars_struct = construct %a, %b, undef
+    %8:tint_module_vars_struct = construct %a, %b, unused
     %tint_module_vars_1:tint_module_vars_struct = let %8  # %tint_module_vars_1: 'tint_module_vars'
     %10:ptr<storage, i32, read_write> = access %tint_module_vars_1, 1u
     %11:i32 = load %10
@@ -1479,7 +1479,7 @@
 %main_b = @fragment func(%a_1:ptr<uniform, i32, read> [@binding_point(1, 2)]):void {  # %a_1: 'a'
   $B3: {
     %c:ptr<private, i32, read_write> = var
-    %18:tint_module_vars_struct = construct %a_1, undef, %c
+    %18:tint_module_vars_struct = construct %a_1, unused, %c
     %tint_module_vars_2:tint_module_vars_struct = let %18  # %tint_module_vars_2: 'tint_module_vars'
     %20:ptr<private, i32, read_write> = access %tint_module_vars_2, 2u
     %21:i32 = load %20