[tint][ir] Use intrinsic table for unary ops.

Much like BuiltinCall, make ir::Unary abstract, with a derived class for each dialect.
Add ir::CoreUnary to hold the core-dialect unary operators.
Make the validator consult the dialet's intrinsic table.

Change-Id: I08331410c1ea318f6681b90f42ebe9923e27eed2
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/168225
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: James Price <jrprice@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
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 3c62fe9..07d5897 100644
--- a/src/tint/lang/core/ir/BUILD.bazel
+++ b/src/tint/lang/core/ir/BUILD.bazel
@@ -55,6 +55,7 @@
     "control_instruction.cc",
     "convert.cc",
     "core_builtin_call.cc",
+    "core_unary.cc",
     "disassembler.cc",
     "discard.cc",
     "exit.cc",
@@ -105,6 +106,7 @@
     "control_instruction.h",
     "convert.h",
     "core_builtin_call.h",
+    "core_unary.h",
     "disassembler.h",
     "discard.h",
     "exit.h",
@@ -179,6 +181,7 @@
     "continue_test.cc",
     "convert_test.cc",
     "core_builtin_call_test.cc",
+    "core_unary_test.cc",
     "discard_test.cc",
     "exit_if_test.cc",
     "exit_loop_test.cc",
@@ -204,7 +207,6 @@
     "swizzle_test.cc",
     "terminate_invocation_test.cc",
     "traverse_test.cc",
-    "unary_test.cc",
     "unreachable_test.cc",
     "user_call_test.cc",
     "validator_test.cc",
diff --git a/src/tint/lang/core/ir/BUILD.cmake b/src/tint/lang/core/ir/BUILD.cmake
index c901636..33f118e 100644
--- a/src/tint/lang/core/ir/BUILD.cmake
+++ b/src/tint/lang/core/ir/BUILD.cmake
@@ -74,6 +74,8 @@
   lang/core/ir/convert.h
   lang/core/ir/core_builtin_call.cc
   lang/core/ir/core_builtin_call.h
+  lang/core/ir/core_unary.cc
+  lang/core/ir/core_unary.h
   lang/core/ir/disassembler.cc
   lang/core/ir/disassembler.h
   lang/core/ir/discard.cc
@@ -180,6 +182,7 @@
   lang/core/ir/continue_test.cc
   lang/core/ir/convert_test.cc
   lang/core/ir/core_builtin_call_test.cc
+  lang/core/ir/core_unary_test.cc
   lang/core/ir/discard_test.cc
   lang/core/ir/exit_if_test.cc
   lang/core/ir/exit_loop_test.cc
@@ -205,7 +208,6 @@
   lang/core/ir/swizzle_test.cc
   lang/core/ir/terminate_invocation_test.cc
   lang/core/ir/traverse_test.cc
-  lang/core/ir/unary_test.cc
   lang/core/ir/unreachable_test.cc
   lang/core/ir/user_call_test.cc
   lang/core/ir/validator_test.cc
diff --git a/src/tint/lang/core/ir/BUILD.gn b/src/tint/lang/core/ir/BUILD.gn
index 9bcb139..2fbb5ab 100644
--- a/src/tint/lang/core/ir/BUILD.gn
+++ b/src/tint/lang/core/ir/BUILD.gn
@@ -76,6 +76,8 @@
     "convert.h",
     "core_builtin_call.cc",
     "core_builtin_call.h",
+    "core_unary.cc",
+    "core_unary.h",
     "disassembler.cc",
     "disassembler.h",
     "discard.cc",
@@ -179,6 +181,7 @@
       "continue_test.cc",
       "convert_test.cc",
       "core_builtin_call_test.cc",
+      "core_unary_test.cc",
       "discard_test.cc",
       "exit_if_test.cc",
       "exit_loop_test.cc",
@@ -204,7 +207,6 @@
       "swizzle_test.cc",
       "terminate_invocation_test.cc",
       "traverse_test.cc",
-      "unary_test.cc",
       "unreachable_test.cc",
       "user_call_test.cc",
       "validator_test.cc",
diff --git a/src/tint/lang/core/ir/binary/decode.cc b/src/tint/lang/core/ir/binary/decode.cc
index fb77a96..d5fd41f 100644
--- a/src/tint/lang/core/ir/binary/decode.cc
+++ b/src/tint/lang/core/ir/binary/decode.cc
@@ -494,8 +494,8 @@
         return switch_out;
     }
 
-    ir::Unary* CreateInstructionUnary(const pb::InstructionUnary& unary_in) {
-        auto* unary_out = mod_out_.instructions.Create<ir::Unary>();
+    ir::CoreUnary* CreateInstructionUnary(const pb::InstructionUnary& unary_in) {
+        auto* unary_out = mod_out_.instructions.Create<ir::CoreUnary>();
         unary_out->SetOp(UnaryOp(unary_in.op()));
         return unary_out;
     }
@@ -894,16 +894,22 @@
         }
     }
 
-    core::ir::UnaryOp UnaryOp(pb::UnaryOp in) {
+    core::UnaryOp UnaryOp(pb::UnaryOp in) {
         switch (in) {
             case pb::UnaryOp::complement:
-                return core::ir::UnaryOp::kComplement;
+                return core::UnaryOp::kComplement;
             case pb::UnaryOp::negation:
-                return core::ir::UnaryOp::kNegation;
+                return core::UnaryOp::kNegation;
+            case pb::UnaryOp::address_of:
+                return core::UnaryOp::kAddressOf;
+            case pb::UnaryOp::indirection:
+                return core::UnaryOp::kIndirection;
+            case pb::UnaryOp::not_:
+                return core::UnaryOp::kNot;
 
             default:
                 TINT_ICE() << "invalid UnaryOp: " << in;
-                return core::ir::UnaryOp::kComplement;
+                return core::UnaryOp::kComplement;
         }
     }
 
diff --git a/src/tint/lang/core/ir/binary/encode.cc b/src/tint/lang/core/ir/binary/encode.cc
index fa72a55..cb45846 100644
--- a/src/tint/lang/core/ir/binary/encode.cc
+++ b/src/tint/lang/core/ir/binary/encode.cc
@@ -42,6 +42,7 @@
 #include "src/tint/lang/core/ir/continue.h"
 #include "src/tint/lang/core/ir/convert.h"
 #include "src/tint/lang/core/ir/core_builtin_call.h"
+#include "src/tint/lang/core/ir/core_unary.h"
 #include "src/tint/lang/core/ir/discard.h"
 #include "src/tint/lang/core/ir/exit_if.h"
 #include "src/tint/lang/core/ir/exit_loop.h"
@@ -60,7 +61,6 @@
 #include "src/tint/lang/core/ir/store_vector_element.h"
 #include "src/tint/lang/core/ir/switch.h"
 #include "src/tint/lang/core/ir/swizzle.h"
-#include "src/tint/lang/core/ir/unary.h"
 #include "src/tint/lang/core/ir/unreachable.h"
 #include "src/tint/lang/core/ir/user_call.h"
 #include "src/tint/lang/core/ir/var.h"
@@ -230,7 +230,7 @@
             },
             [&](const ir::Switch* i) { InstructionSwitch(*inst_out.mutable_switch_(), i); },
             [&](const ir::Swizzle* i) { InstructionSwizzle(*inst_out.mutable_swizzle(), i); },
-            [&](const ir::Unary* i) { InstructionUnary(*inst_out.mutable_unary(), i); },
+            [&](const ir::CoreUnary* i) { InstructionUnary(*inst_out.mutable_unary(), i); },
             [&](const ir::UserCall* i) { InstructionUserCall(*inst_out.mutable_user_call(), i); },
             [&](const ir::Var* i) { InstructionVar(*inst_out.mutable_var(), i); },
             [&](const ir::Unreachable* i) {
@@ -329,7 +329,7 @@
         }
     }
 
-    void InstructionUnary(pb::InstructionUnary& unary_out, const ir::Unary* unary_in) {
+    void InstructionUnary(pb::InstructionUnary& unary_out, const ir::CoreUnary* unary_in) {
         unary_out.set_op(UnaryOp(unary_in->Op()));
     }
 
@@ -675,12 +675,18 @@
         }
     }
 
-    pb::UnaryOp UnaryOp(core::ir::UnaryOp in) {
+    pb::UnaryOp UnaryOp(core::UnaryOp in) {
         switch (in) {
-            case core::ir::UnaryOp::kComplement:
+            case core::UnaryOp::kComplement:
                 return pb::UnaryOp::complement;
-            case core::ir::UnaryOp::kNegation:
+            case core::UnaryOp::kNegation:
                 return pb::UnaryOp::negation;
+            case core::UnaryOp::kAddressOf:
+                return pb::UnaryOp::address_of;
+            case core::UnaryOp::kIndirection:
+                return pb::UnaryOp::indirection;
+            case core::UnaryOp::kNot:
+                return pb::UnaryOp::not_;
         }
         TINT_ICE() << "invalid UnaryOp: " << in;
         return pb::UnaryOp::complement;
diff --git a/src/tint/lang/core/ir/binary/ir.proto b/src/tint/lang/core/ir/binary/ir.proto
index 8eb5c20..b964c92 100644
--- a/src/tint/lang/core/ir/binary/ir.proto
+++ b/src/tint/lang/core/ir/binary/ir.proto
@@ -413,6 +413,9 @@
 enum UnaryOp {
     complement = 0;
     negation = 1;
+    address_of = 2;
+    indirection = 3;
+    not = 4;
 }
 
 enum BinaryOp {
diff --git a/src/tint/lang/core/ir/builder.h b/src/tint/lang/core/ir/builder.h
index fc847c7..5e8f502 100644
--- a/src/tint/lang/core/ir/builder.h
+++ b/src/tint/lang/core/ir/builder.h
@@ -43,6 +43,7 @@
 #include "src/tint/lang/core/ir/continue.h"
 #include "src/tint/lang/core/ir/convert.h"
 #include "src/tint/lang/core/ir/core_builtin_call.h"
+#include "src/tint/lang/core/ir/core_unary.h"
 #include "src/tint/lang/core/ir/discard.h"
 #include "src/tint/lang/core/ir/exit_if.h"
 #include "src/tint/lang/core/ir/exit_loop.h"
@@ -64,7 +65,6 @@
 #include "src/tint/lang/core/ir/switch.h"
 #include "src/tint/lang/core/ir/swizzle.h"
 #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/user_call.h"
 #include "src/tint/lang/core/ir/value.h"
@@ -824,9 +824,9 @@
     /// @param val the value of the operation
     /// @returns the operation
     template <typename VAL>
-    ir::Unary* Unary(UnaryOp op, const core::type::Type* type, VAL&& val) {
+    ir::CoreUnary* Unary(UnaryOp op, const core::type::Type* type, VAL&& val) {
         auto* value = Value(std::forward<VAL>(val));
-        return Append(ir.instructions.Create<ir::Unary>(InstructionResult(type), op, value));
+        return Append(ir.instructions.Create<ir::CoreUnary>(InstructionResult(type), op, value));
     }
 
     /// Creates an op for `op val`
@@ -835,7 +835,7 @@
     /// @param val the value of the operation
     /// @returns the operation
     template <typename TYPE, typename VAL>
-    ir::Unary* Unary(UnaryOp op, VAL&& val) {
+    ir::CoreUnary* Unary(UnaryOp op, VAL&& val) {
         auto* type = ir.Types().Get<TYPE>();
         return Unary(op, type, std::forward<VAL>(val));
     }
@@ -845,8 +845,8 @@
     /// @param val the value
     /// @returns the operation
     template <typename VAL>
-    ir::Unary* Complement(const core::type::Type* type, VAL&& val) {
-        return Unary(ir::UnaryOp::kComplement, type, std::forward<VAL>(val));
+    ir::CoreUnary* Complement(const core::type::Type* type, VAL&& val) {
+        return Unary(UnaryOp::kComplement, type, std::forward<VAL>(val));
     }
 
     /// Creates a Complement operation
@@ -854,7 +854,7 @@
     /// @param val the value
     /// @returns the operation
     template <typename TYPE, typename VAL>
-    ir::Unary* Complement(VAL&& val) {
+    ir::CoreUnary* Complement(VAL&& val) {
         auto* type = ir.Types().Get<TYPE>();
         return Complement(type, std::forward<VAL>(val));
     }
@@ -864,8 +864,8 @@
     /// @param val the value
     /// @returns the operation
     template <typename VAL>
-    ir::Unary* Negation(const core::type::Type* type, VAL&& val) {
-        return Unary(ir::UnaryOp::kNegation, type, std::forward<VAL>(val));
+    ir::CoreUnary* Negation(const core::type::Type* type, VAL&& val) {
+        return Unary(UnaryOp::kNegation, type, std::forward<VAL>(val));
     }
 
     /// Creates a Negation operation
@@ -873,7 +873,7 @@
     /// @param val the value
     /// @returns the operation
     template <typename TYPE, typename VAL>
-    ir::Unary* Negation(VAL&& val) {
+    ir::CoreUnary* Negation(VAL&& val) {
         auto* type = ir.Types().Get<TYPE>();
         return Negation(type, std::forward<VAL>(val));
     }
diff --git a/src/tint/lang/core/ir/core_unary.cc b/src/tint/lang/core/ir/core_unary.cc
new file mode 100644
index 0000000..5ab130d
--- /dev/null
+++ b/src/tint/lang/core/ir/core_unary.cc
@@ -0,0 +1,54 @@
+// 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/core_unary.h"
+
+#include "src/tint/lang/core/intrinsic/dialect.h"
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::core::ir::CoreUnary);
+
+namespace tint::core::ir {
+
+CoreUnary::CoreUnary() = default;
+
+CoreUnary::CoreUnary(InstructionResult* result, UnaryOp op, Value* val) : Base(result, op, val) {}
+
+CoreUnary::~CoreUnary() = default;
+
+CoreUnary* CoreUnary::Clone(CloneContext& ctx) {
+    auto* new_result = ctx.Clone(Result(0));
+    auto* val = ctx.Remap(Val());
+    return ctx.ir.instructions.Create<CoreUnary>(new_result, Op(), val);
+}
+
+const core::intrinsic::TableData& CoreUnary::TableData() const {
+    return core::intrinsic::Dialect::kData;
+}
+
+}  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/core_unary.h b/src/tint/lang/core/ir/core_unary.h
new file mode 100644
index 0000000..04a02f4
--- /dev/null
+++ b/src/tint/lang/core/ir/core_unary.h
@@ -0,0 +1,60 @@
+// 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_CORE_UNARY_H_
+#define SRC_TINT_LANG_CORE_IR_CORE_UNARY_H_
+
+#include "src/tint/lang/core/ir/unary.h"
+
+namespace tint::core::ir {
+
+/// A core-dialect unary instruction in the IR.
+class CoreUnary final : public Castable<CoreUnary, Unary> {
+  public:
+    /// The offset in Operands() for the value
+    static constexpr size_t kValueOperandOffset = 0;
+
+    /// Constructor (no results, no operands)
+    CoreUnary();
+
+    /// Constructor
+    /// @param result the result value
+    /// @param op the unary operator
+    /// @param val the input value for the instruction
+    CoreUnary(InstructionResult* result, UnaryOp op, Value* val);
+    ~CoreUnary() override;
+
+    /// @copydoc Instruction::Clone()
+    CoreUnary* Clone(CloneContext& ctx) override;
+
+    /// @returns the table data to validate this builtin
+    const core::intrinsic::TableData& TableData() const override;
+};
+
+}  // namespace tint::core::ir
+
+#endif  // SRC_TINT_LANG_CORE_IR_CORE_UNARY_H_
diff --git a/src/tint/lang/core/ir/unary_test.cc b/src/tint/lang/core/ir/core_unary_test.cc
similarity index 100%
rename from src/tint/lang/core/ir/unary_test.cc
rename to src/tint/lang/core/ir/core_unary_test.cc
diff --git a/src/tint/lang/core/ir/disassembler.cc b/src/tint/lang/core/ir/disassembler.cc
index 9292f50..ee66e2b 100644
--- a/src/tint/lang/core/ir/disassembler.cc
+++ b/src/tint/lang/core/ir/disassembler.cc
@@ -870,6 +870,15 @@
         case UnaryOp::kNegation:
             out_ << "negation";
             break;
+        case UnaryOp::kAddressOf:
+            out_ << "ref-to-ptr";
+            break;
+        case UnaryOp::kIndirection:
+            out_ << "ptr-to-ref";
+            break;
+        case UnaryOp::kNot:
+            out_ << "not";
+            break;
     }
     out_ << " ";
     EmitOperandList(u);
diff --git a/src/tint/lang/core/ir/unary.cc b/src/tint/lang/core/ir/unary.cc
index d4de4e1..9fdfe0f 100644
--- a/src/tint/lang/core/ir/unary.cc
+++ b/src/tint/lang/core/ir/unary.cc
@@ -43,19 +43,4 @@
 
 Unary::~Unary() = default;
 
-Unary* Unary::Clone(CloneContext& ctx) {
-    auto* new_result = ctx.Clone(Result(0));
-    auto* val = ctx.Remap(Val());
-    return ctx.ir.instructions.Create<Unary>(new_result, op_, val);
-}
-
-std::string_view ToString(enum UnaryOp op) {
-    switch (op) {
-        case UnaryOp::kComplement:
-            return "complement";
-        case UnaryOp::kNegation:
-            return "negation";
-    }
-    return "<unknown>";
-}
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/unary.h b/src/tint/lang/core/ir/unary.h
index 41a4875..7cd4b37 100644
--- a/src/tint/lang/core/ir/unary.h
+++ b/src/tint/lang/core/ir/unary.h
@@ -31,18 +31,17 @@
 #include <string>
 
 #include "src/tint/lang/core/ir/operand_instruction.h"
-#include "src/tint/utils/rtti/castable.h"
+#include "src/tint/lang/core/unary_op.h"
+
+// Forward declarations
+namespace tint::core::intrinsic {
+struct TableData;
+}
 
 namespace tint::core::ir {
 
-/// A unary operator.
-enum class UnaryOp {
-    kComplement,
-    kNegation,
-};
-
 /// A unary instruction in the IR.
-class Unary final : public Castable<Unary, OperandInstruction<1, 1>> {
+class Unary : public Castable<Unary, OperandInstruction<1, 1>> {
   public:
     /// The offset in Operands() for the value
     static constexpr size_t kValueOperandOffset = 0;
@@ -57,9 +56,6 @@
     Unary(InstructionResult* result, UnaryOp op, Value* val);
     ~Unary() override;
 
-    /// @copydoc Instruction::Clone()
-    Unary* Clone(CloneContext& ctx) override;
-
     /// @returns the value for the instruction
     Value* Val() { return operands_[kValueOperandOffset]; }
 
@@ -75,20 +71,13 @@
     /// @returns the friendly name for the instruction
     std::string FriendlyName() const override { return "unary"; }
 
+    /// @returns the table data to validate this builtin
+    virtual const core::intrinsic::TableData& TableData() const = 0;
+
   private:
     UnaryOp op_ = UnaryOp::kComplement;
 };
 
-/// @param kind the enum value
-/// @returns the string for the given enum value
-std::string_view ToString(UnaryOp kind);
-
-/// Emits the name of the intrinsic type.
-template <typename STREAM, typename = traits::EnableIfIsOStream<STREAM>>
-auto& operator<<(STREAM& out, UnaryOp kind) {
-    return out << ToString(kind);
-}
-
 }  // namespace tint::core::ir
 
 #endif  // SRC_TINT_LANG_CORE_IR_UNARY_H_
diff --git a/src/tint/lang/core/ir/validator.cc b/src/tint/lang/core/ir/validator.cc
index dd7925a..57180b3 100644
--- a/src/tint/lang/core/ir/validator.cc
+++ b/src/tint/lang/core/ir/validator.cc
@@ -671,10 +671,30 @@
 
 void Validator::CheckUnary(const Unary* u) {
     CheckOperandNotNull(u, u->Val(), Unary::kValueOperandOffset);
+    if (u->Val()) {
+        auto symbols = SymbolTable::Wrap(mod_.symbols);
+        auto type_mgr = type::Manager::Wrap(mod_.Types());
+        intrinsic::Context context{
+            u->TableData(),
+            type_mgr,
+            symbols,
+        };
 
-    if (u->Result(0) && u->Val()) {
-        if (u->Result(0)->Type() != u->Val()->Type()) {
-            AddError(u, InstError(u, "result type must match value type"));
+        auto overload = core::intrinsic::LookupUnary(context, u->Op(), u->Val()->Type(),
+                                                     core::EvaluationStage::kRuntime);
+        if (overload != Success) {
+            AddError(u, InstError(u, overload.Failure()));
+            return;
+        }
+
+        if (auto* result = u->Result(0)) {
+            if (overload->return_type != result->Type()) {
+                StringStream err;
+                err << "unary instruction result type (" << result->Type()->FriendlyName()
+                    << ") does not match overload result type ("
+                    << overload->return_type->FriendlyName() << ")";
+                AddError(u, InstError(u, err.str()));
+            }
         }
     }
 }
diff --git a/src/tint/lang/core/ir/validator_test.cc b/src/tint/lang/core/ir/validator_test.cc
index 7dd1d69..3f2fd5c 100644
--- a/src/tint/lang/core/ir/validator_test.cc
+++ b/src/tint/lang/core/ir/validator_test.cc
@@ -1329,7 +1329,7 @@
 
 TEST_F(IR_ValidatorTest, Unary_Result_Nullptr) {
     auto* bin =
-        mod.instructions.Create<ir::Unary>(nullptr, ir::UnaryOp::kNegation, b.Constant(2_i));
+        mod.instructions.Create<ir::CoreUnary>(nullptr, UnaryOp::kNegation, b.Constant(2_i));
 
     auto* f = b.Function("my_func", ty.void_());
 
@@ -1368,7 +1368,9 @@
 
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: unary: result type must match value type
+    EXPECT_EQ(
+        res.Failure().reason.str(),
+        R"(:3:5 error: unary: unary instruction result type (f32) does not match overload result type (i32)
     %2:f32 = complement 2i
     ^^^^^^^^^^^^^^^^^^^^^^
 
diff --git a/src/tint/lang/msl/writer/printer/printer.cc b/src/tint/lang/msl/writer/printer/printer.cc
index 6cfa838..dfc8eda 100644
--- a/src/tint/lang/msl/writer/printer/printer.cc
+++ b/src/tint/lang/msl/writer/printer/printer.cc
@@ -43,6 +43,7 @@
 #include "src/tint/lang/core/ir/continue.h"
 #include "src/tint/lang/core/ir/convert.h"
 #include "src/tint/lang/core/ir/core_builtin_call.h"
+#include "src/tint/lang/core/ir/core_unary.h"
 #include "src/tint/lang/core/ir/discard.h"
 #include "src/tint/lang/core/ir/exit_if.h"
 #include "src/tint/lang/core/ir/exit_loop.h"
@@ -349,7 +350,7 @@
                 [&](core::ir::LoadVectorElement*) { /* inlined */ },  //
                 [&](core::ir::Swizzle*) { /* inlined */ },            //
                 [&](core::ir::Bitcast*) { /* inlined */ },            //
-                [&](core::ir::Unary*) { /* inlined */ },              //
+                [&](core::ir::CoreUnary*) { /* inlined */ },          //
                 [&](core::ir::Binary*) { /* inlined */ },             //
                 [&](core::ir::Load*) { /* inlined */ },               //
                 [&](core::ir::Construct*) { /* inlined */ },          //
@@ -365,7 +366,7 @@
             [&](const core::ir::InstructionResult* r) {
                 Switch(
                     r->Instruction(),                                                          //
-                    [&](const core::ir::Unary* u) { EmitUnary(out, u); },                      //
+                    [&](const core::ir::CoreUnary* u) { EmitUnary(out, u); },                  //
                     [&](const core::ir::Binary* b) { EmitBinary(out, b); },                    //
                     [&](const core::ir::Convert* b) { EmitConvert(out, b); },                  //
                     [&](const core::ir::Let* l) { out << NameOf(l->Result(0)); },              //
@@ -387,14 +388,17 @@
             TINT_ICE_ON_NO_MATCH);
     }
 
-    void EmitUnary(StringStream& out, const core::ir::Unary* u) {
+    void EmitUnary(StringStream& out, const core::ir::CoreUnary* u) {
         switch (u->Op()) {
-            case core::ir::UnaryOp::kNegation:
+            case core::UnaryOp::kNegation:
                 out << "-";
                 break;
-            case core::ir::UnaryOp::kComplement:
+            case core::UnaryOp::kComplement:
                 out << "~";
                 break;
+            default:
+                TINT_UNIMPLEMENTED() << u->Op();
+                break;
         }
         out << "(";
         EmitValue(out, u->Val());
diff --git a/src/tint/lang/spirv/writer/printer/printer.cc b/src/tint/lang/spirv/writer/printer/printer.cc
index e5e9945..7863fd8 100644
--- a/src/tint/lang/spirv/writer/printer/printer.cc
+++ b/src/tint/lang/spirv/writer/printer/printer.cc
@@ -893,7 +893,7 @@
                 [&](core::ir::Store* s) { EmitStore(s); },                            //
                 [&](core::ir::StoreVectorElement* s) { EmitStoreVectorElement(s); },  //
                 [&](core::ir::UserCall* c) { EmitUserCall(c); },                      //
-                [&](core::ir::Unary* u) { EmitUnary(u); },                            //
+                [&](core::ir::CoreUnary* u) { EmitUnary(u); },                        //
                 [&](core::ir::Var* v) { EmitVar(v); },                                //
                 [&](core::ir::Let* l) { EmitLet(l); },                                //
                 [&](core::ir::If* i) { EmitIf(i); },                                  //
@@ -1961,21 +1961,24 @@
 
     /// Emit a unary instruction.
     /// @param unary the unary instruction to emit
-    void EmitUnary(core::ir::Unary* unary) {
+    void EmitUnary(core::ir::CoreUnary* unary) {
         auto id = Value(unary);
         auto* ty = unary->Result(0)->Type();
         spv::Op op = spv::Op::Max;
         switch (unary->Op()) {
-            case core::ir::UnaryOp::kComplement:
+            case core::UnaryOp::kComplement:
                 op = spv::Op::OpNot;
                 break;
-            case core::ir::UnaryOp::kNegation:
+            case core::UnaryOp::kNegation:
                 if (ty->is_float_scalar_or_vector()) {
                     op = spv::Op::OpFNegate;
                 } else if (ty->is_signed_integer_scalar_or_vector()) {
                     op = spv::Op::OpSNegate;
                 }
                 break;
+            default:
+                TINT_UNIMPLEMENTED() << unary->Op();
+                break;
         }
         current_function_.push_inst(op, {Type(ty), id, Value(unary->Val())});
     }
diff --git a/src/tint/lang/spirv/writer/unary_test.cc b/src/tint/lang/spirv/writer/unary_test.cc
index 72fff71..43fb04e 100644
--- a/src/tint/lang/spirv/writer/unary_test.cc
+++ b/src/tint/lang/spirv/writer/unary_test.cc
@@ -39,7 +39,7 @@
     /// The element type to test.
     TestElementType type;
     /// The unary operation.
-    enum core::ir::UnaryOp op;
+    core::UnaryOp op;
     /// The expected SPIR-V instruction.
     std::string spirv_inst;
     /// The expected SPIR-V result type name.
@@ -80,11 +80,11 @@
 INSTANTIATE_TEST_SUITE_P(
     SpirvWriterTest_Unary,
     Arithmetic,
-    testing::Values(UnaryTestCase{kI32, core::ir::UnaryOp::kComplement, "OpNot", "int"},
-                    UnaryTestCase{kU32, core::ir::UnaryOp::kComplement, "OpNot", "uint"},
-                    UnaryTestCase{kI32, core::ir::UnaryOp::kNegation, "OpSNegate", "int"},
-                    UnaryTestCase{kF32, core::ir::UnaryOp::kNegation, "OpFNegate", "float"},
-                    UnaryTestCase{kF16, core::ir::UnaryOp::kNegation, "OpFNegate", "half"}));
+    testing::Values(UnaryTestCase{kI32, core::UnaryOp::kComplement, "OpNot", "int"},
+                    UnaryTestCase{kU32, core::UnaryOp::kComplement, "OpNot", "uint"},
+                    UnaryTestCase{kI32, core::UnaryOp::kNegation, "OpSNegate", "int"},
+                    UnaryTestCase{kF32, core::UnaryOp::kNegation, "OpFNegate", "float"},
+                    UnaryTestCase{kF16, core::UnaryOp::kNegation, "OpFNegate", "half"}));
 
 }  // namespace
 }  // namespace tint::spirv::writer
diff --git a/src/tint/lang/wgsl/ir/BUILD.bazel b/src/tint/lang/wgsl/ir/BUILD.bazel
index e764df7..259b851 100644
--- a/src/tint/lang/wgsl/ir/BUILD.bazel
+++ b/src/tint/lang/wgsl/ir/BUILD.bazel
@@ -40,9 +40,11 @@
   name = "ir",
   srcs = [
     "builtin_call.cc",
+    "unary.cc",
   ],
   hdrs = [
     "builtin_call.h",
+    "unary.h",
   ],
   deps = [
     "//src/tint/api/common",
diff --git a/src/tint/lang/wgsl/ir/BUILD.cmake b/src/tint/lang/wgsl/ir/BUILD.cmake
index f41c7a4..d0192da 100644
--- a/src/tint/lang/wgsl/ir/BUILD.cmake
+++ b/src/tint/lang/wgsl/ir/BUILD.cmake
@@ -41,6 +41,8 @@
 tint_add_target(tint_lang_wgsl_ir lib
   lang/wgsl/ir/builtin_call.cc
   lang/wgsl/ir/builtin_call.h
+  lang/wgsl/ir/unary.cc
+  lang/wgsl/ir/unary.h
 )
 
 tint_target_add_dependencies(tint_lang_wgsl_ir lib
diff --git a/src/tint/lang/wgsl/ir/BUILD.gn b/src/tint/lang/wgsl/ir/BUILD.gn
index 462ecfa..1194bb7 100644
--- a/src/tint/lang/wgsl/ir/BUILD.gn
+++ b/src/tint/lang/wgsl/ir/BUILD.gn
@@ -42,6 +42,8 @@
   sources = [
     "builtin_call.cc",
     "builtin_call.h",
+    "unary.cc",
+    "unary.h",
   ]
   deps = [
     "${tint_src_dir}/api/common",
diff --git a/src/tint/lang/wgsl/ir/unary.cc b/src/tint/lang/wgsl/ir/unary.cc
new file mode 100644
index 0000000..f8c90f3
--- /dev/null
+++ b/src/tint/lang/wgsl/ir/unary.cc
@@ -0,0 +1,55 @@
+// 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/wgsl/ir/unary.h"
+
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+#include "src/tint/lang/wgsl/intrinsic/dialect.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::wgsl::ir::Unary);
+
+namespace tint::wgsl::ir {
+
+Unary::Unary() = default;
+
+Unary::Unary(core::ir::InstructionResult* result, core::UnaryOp op, core::ir::Value* val)
+    : Base(result, op, val) {}
+
+Unary::~Unary() = default;
+
+Unary* Unary::Clone(core::ir::CloneContext& ctx) {
+    auto* new_result = ctx.Clone(Result(0));
+    auto* val = ctx.Remap(Val());
+    return ctx.ir.instructions.Create<Unary>(new_result, Op(), val);
+}
+
+const core::intrinsic::TableData& Unary::TableData() const {
+    return wgsl::intrinsic::Dialect::kData;
+}
+
+}  // namespace tint::wgsl::ir
diff --git a/src/tint/lang/wgsl/ir/unary.h b/src/tint/lang/wgsl/ir/unary.h
new file mode 100644
index 0000000..9fbeeea
--- /dev/null
+++ b/src/tint/lang/wgsl/ir/unary.h
@@ -0,0 +1,60 @@
+// 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_WGSL_IR_UNARY_H_
+#define SRC_TINT_LANG_WGSL_IR_UNARY_H_
+
+#include "src/tint/lang/core/ir/unary.h"
+
+namespace tint::wgsl::ir {
+
+/// A WGSL-dialect unary instruction in the IR.
+class Unary final : public Castable<Unary, core::ir::Unary> {
+  public:
+    /// The offset in Operands() for the value
+    static constexpr size_t kValueOperandOffset = 0;
+
+    /// Constructor (no results, no operands)
+    Unary();
+
+    /// Constructor
+    /// @param result the result value
+    /// @param op the unary operator
+    /// @param val the input value for the instruction
+    Unary(core::ir::InstructionResult* result, core::UnaryOp op, core::ir::Value* val);
+    ~Unary() override;
+
+    /// @copydoc core::ir::Instruction::Clone()
+    Unary* Clone(core::ir::CloneContext& ctx) override;
+
+    /// @returns the table data to validate this builtin
+    const core::intrinsic::TableData& TableData() const override;
+};
+
+}  // namespace tint::wgsl::ir
+
+#endif  // SRC_TINT_LANG_WGSL_IR_UNARY_H_
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
index f7b6321..642617c 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
@@ -749,12 +749,15 @@
     void Unary(const core::ir::Unary* u) {
         const ast::Expression* expr = nullptr;
         switch (u->Op()) {
-            case core::ir::UnaryOp::kComplement:
+            case core::UnaryOp::kComplement:
                 expr = b.Complement(Expr(u->Val()));
                 break;
-            case core::ir::UnaryOp::kNegation:
+            case core::UnaryOp::kNegation:
                 expr = b.Negation(Expr(u->Val()));
                 break;
+            default:
+                TINT_UNIMPLEMENTED() << u->Op();
+                break;
         }
         Bind(u->Result(0), expr);
     }