IntrinsicTable: De-duplicate returned Intrinsics

Much like sem::Type, it greatly simplifies downstream logic if we can compare sem::Intrinsic pointers to know if they refer to the same intrinsic overload.

Change-Id: If236247cd3979bbde821d9294f304ab85ba4938e
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/58061
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: David Neto <dneto@google.com>
diff --git a/src/inspector/sampler_texture_pair.h b/src/inspector/sampler_texture_pair.h
index 12b4f7e..2af9fbe 100644
--- a/src/inspector/sampler_texture_pair.h
+++ b/src/inspector/sampler_texture_pair.h
@@ -51,8 +51,8 @@
 
 namespace std {
 
-/// Custom std::hash specialization for ttint::inspector::SamplerTexturePair so
-/// SamplerTexturePairss be used as keys for std::unordered_map and
+/// Custom std::hash specialization for tint::inspector::SamplerTexturePair so
+/// SamplerTexturePairs be used as keys for std::unordered_map and
 /// std::unordered_set.
 template <>
 class hash<tint::inspector::SamplerTexturePair> {
diff --git a/src/intrinsic_table.cc b/src/intrinsic_table.cc
index 0997eae..233aff4 100644
--- a/src/intrinsic_table.cc
+++ b/src/intrinsic_table.cc
@@ -27,6 +27,8 @@
 #include "src/sem/pipeline_stage_set.h"
 #include "src/sem/sampled_texture_type.h"
 #include "src/sem/storage_texture_type.h"
+#include "src/utils/get_or_create.h"
+#include "src/utils/hash.h"
 #include "src/utils/scoped_assignment.h"
 
 namespace tint {
@@ -706,13 +708,13 @@
 
   const sem::Intrinsic* Lookup(sem::IntrinsicType intrinsic_type,
                                const std::vector<const sem::Type*>& args,
-                               const Source& source) const override;
+                               const Source& source) override;
 
  private:
   const sem::Intrinsic* Match(sem::IntrinsicType intrinsic_type,
                               const OverloadInfo& overload,
                               const std::vector<const sem::Type*>& args,
-                              int& match_score) const;
+                              int& match_score);
 
   MatchState Match(ClosedState& closed,
                    const OverloadInfo& overload,
@@ -724,6 +726,7 @@
 
   ProgramBuilder& builder;
   Matchers matchers;
+  std::unordered_map<sem::Intrinsic, sem::Intrinsic*> intrinsics;
 };
 
 /// @return a string representing a call to an intrinsic with the given argument
@@ -760,7 +763,7 @@
 
 const sem::Intrinsic* Impl::Lookup(sem::IntrinsicType intrinsic_type,
                                    const std::vector<const sem::Type*>& args,
-                                   const Source& source) const {
+                                   const Source& source) {
   // Candidate holds information about a mismatched overload that could be what
   // the user intended to call.
   struct Candidate {
@@ -809,7 +812,7 @@
 const sem::Intrinsic* Impl::Match(sem::IntrinsicType intrinsic_type,
                                   const OverloadInfo& overload,
                                   const std::vector<const sem::Type*>& args,
-                                  int& match_score) const {
+                                  int& match_score) {
   // Score wait for argument <-> parameter count matches / mismatches
   constexpr int kScorePerParamArgMismatch = -1;
   constexpr int kScorePerMatchedParam = 2;
@@ -896,9 +899,14 @@
     return_type = builder.create<sem::Void>();
   }
 
-  return builder.create<sem::Intrinsic>(
-      intrinsic_type, const_cast<sem::Type*>(return_type),
-      std::move(parameters), overload.supported_stages, overload.is_deprecated);
+  sem::Intrinsic intrinsic(intrinsic_type, const_cast<sem::Type*>(return_type),
+                           std::move(parameters), overload.supported_stages,
+                           overload.is_deprecated);
+
+  // De-duplicate intrinsics that are identical.
+  return utils::GetOrCreate(intrinsics, intrinsic, [&] {
+    return builder.create<sem::Intrinsic>(intrinsic);
+  });
 }
 
 MatchState Impl::Match(ClosedState& closed,
diff --git a/src/intrinsic_table.h b/src/intrinsic_table.h
index 6802e55..12532b6 100644
--- a/src/intrinsic_table.h
+++ b/src/intrinsic_table.h
@@ -45,7 +45,7 @@
   virtual const sem::Intrinsic* Lookup(
       sem::IntrinsicType type,
       const std::vector<const sem::Type*>& args,
-      const Source& source) const = 0;
+      const Source& source) = 0;
 };
 
 }  // namespace tint
diff --git a/src/intrinsic_table_test.cc b/src/intrinsic_table_test.cc
index 9a45c24..5ff0654 100644
--- a/src/intrinsic_table_test.cc
+++ b/src/intrinsic_table_test.cc
@@ -548,5 +548,26 @@
 )");
 }
 
+TEST_F(IntrinsicTableTest, SameOverloadReturnsSameIntrinsicPointer) {
+  auto* f32 = create<sem::F32>();
+  auto* vec2_f32 = create<sem::Vector>(create<sem::F32>(), 2);
+  auto* bool_ = create<sem::Bool>();
+  auto* a = table->Lookup(IntrinsicType::kSelect, {f32, f32, bool_}, Source{});
+  ASSERT_NE(a, nullptr) << Diagnostics().str();
+
+  auto* b = table->Lookup(IntrinsicType::kSelect, {f32, f32, bool_}, Source{});
+  ASSERT_NE(b, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+
+  auto* c = table->Lookup(IntrinsicType::kSelect, {vec2_f32, vec2_f32, bool_},
+                          Source{});
+  ASSERT_NE(c, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+
+  EXPECT_EQ(a, b);
+  EXPECT_NE(a, c);
+  EXPECT_NE(b, c);
+}
+
 }  // namespace
 }  // namespace tint
diff --git a/src/sem/call_target.cc b/src/sem/call_target.cc
index ce4c828..5306bf8 100644
--- a/src/sem/call_target.cc
+++ b/src/sem/call_target.cc
@@ -26,6 +26,8 @@
   TINT_ASSERT(Semantic, return_type);
 }
 
+CallTarget::CallTarget(const CallTarget&) = default;
+
 CallTarget::~CallTarget() = default;
 
 int IndexOf(const ParameterList& parameters, ParameterUsage usage) {
diff --git a/src/sem/call_target.h b/src/sem/call_target.h
index 520e22c..e994c80 100644
--- a/src/sem/call_target.h
+++ b/src/sem/call_target.h
@@ -20,6 +20,7 @@
 #include "src/sem/node.h"
 #include "src/sem/parameter_usage.h"
 #include "src/sem/sampler_type.h"
+#include "src/utils/hash.h"
 
 namespace tint {
 
@@ -37,11 +38,16 @@
 
 std::ostream& operator<<(std::ostream& out, Parameter parameter);
 
-/// Comparison operator for Parameters
+/// Equality operator for Parameters
 static inline bool operator==(const Parameter& a, const Parameter& b) {
   return a.type == b.type && a.usage == b.usage;
 }
 
+/// Inequality operator for Parameters
+static inline bool operator!=(const Parameter& a, const Parameter& b) {
+  return !(a == b);
+}
+
 /// ParameterList is a list of Parameter
 using ParameterList = std::vector<Parameter>;
 
@@ -59,6 +65,9 @@
   /// @param parameters the parameters for the call target
   CallTarget(sem::Type* return_type, const ParameterList& parameters);
 
+  /// Copy constructor
+  CallTarget(const CallTarget&);
+
   /// @return the return type of the call target
   sem::Type* ReturnType() const { return return_type_; }
 
@@ -76,4 +85,19 @@
 }  // namespace sem
 }  // namespace tint
 
+namespace std {
+
+/// Custom std::hash specialization for tint::sem::Parameter
+template <>
+class hash<tint::sem::Parameter> {
+ public:
+  /// @param p the tint::sem::Parameter to create a hash for
+  /// @return the hash value
+  inline std::size_t operator()(const tint::sem::Parameter& p) const {
+    return tint::utils::Hash(p.type, p.usage);
+  }
+};
+
+}  // namespace std
+
 #endif  // SRC_SEM_CALL_TARGET_H_
diff --git a/src/sem/intrinsic.cc b/src/sem/intrinsic.cc
index be1a873..4be75e8 100644
--- a/src/sem/intrinsic.cc
+++ b/src/sem/intrinsic.cc
@@ -111,6 +111,8 @@
       supported_stages_(supported_stages),
       is_deprecated_(is_deprecated) {}
 
+Intrinsic::Intrinsic(const Intrinsic&) = default;
+
 Intrinsic::~Intrinsic() = default;
 
 bool Intrinsic::IsCoarseDerivative() const {
@@ -153,5 +155,25 @@
   return IsAtomicIntrinsic(type_);
 }
 
+bool operator==(const Intrinsic& a, const Intrinsic& b) {
+  static_assert(sizeof(Intrinsic(IntrinsicType::kNone, nullptr, ParameterList{},
+                                 PipelineStageSet{}, false)) > 0,
+                "don't forget to update the comparison below if you change the "
+                "constructor of Intrinsic!");
+
+  if (a.Type() != b.Type() || a.SupportedStages() != b.SupportedStages() ||
+      a.ReturnType() != b.ReturnType() ||
+      a.IsDeprecated() != b.IsDeprecated() ||
+      a.Parameters().size() != b.Parameters().size()) {
+    return false;
+  }
+  for (size_t i = 0; i < a.Parameters().size(); i++) {
+    if (a.Parameters()[i] != b.Parameters()[i]) {
+      return false;
+    }
+  }
+  return true;
+}
+
 }  // namespace sem
 }  // namespace tint
diff --git a/src/sem/intrinsic.h b/src/sem/intrinsic.h
index 11cea7b..25c0c59 100644
--- a/src/sem/intrinsic.h
+++ b/src/sem/intrinsic.h
@@ -20,6 +20,7 @@
 #include "src/sem/call_target.h"
 #include "src/sem/intrinsic_type.h"
 #include "src/sem/pipeline_stage_set.h"
+#include "src/utils/hash.h"
 
 namespace tint {
 namespace sem {
@@ -91,6 +92,9 @@
             PipelineStageSet supported_stages,
             bool is_deprecated);
 
+  /// Copy constructor
+  Intrinsic(const Intrinsic&);
+
   /// Destructor
   ~Intrinsic() override;
 
@@ -147,7 +151,31 @@
 /// matches the name in the WGSL spec.
 std::ostream& operator<<(std::ostream& out, IntrinsicType i);
 
+/// Equality operator for Intrinsics
+bool operator==(const Intrinsic& a, const Intrinsic& b);
+
+/// Inequality operator for Intrinsics
+static inline bool operator!=(const Intrinsic& a, const Intrinsic& b) {
+  return !(a == b);
+}
+
 }  // namespace sem
 }  // namespace tint
 
+namespace std {
+
+/// Custom std::hash specialization for tint::sem::Intrinsic
+template <>
+class hash<tint::sem::Intrinsic> {
+ public:
+  /// @param i the Intrinsic to create a hash for
+  /// @return the hash value
+  inline std::size_t operator()(const tint::sem::Intrinsic& i) const {
+    return tint::utils::Hash(i.Type(), i.SupportedStages(), i.ReturnType(),
+                             i.Parameters(), i.IsDeprecated());
+  }
+};
+
+}  // namespace std
+
 #endif  // SRC_SEM_INTRINSIC_H_
diff --git a/src/utils/enum_set.h b/src/utils/enum_set.h
index 396e911..46d93e1 100644
--- a/src/utils/enum_set.h
+++ b/src/utils/enum_set.h
@@ -16,6 +16,7 @@
 #define SRC_UTILS_ENUM_SET_H_
 
 #include <cstdint>
+#include <functional>
 #include <type_traits>
 
 namespace tint {
@@ -58,6 +59,19 @@
   /// @return true if the set contains `e`
   inline bool Contains(Enum e) { return (set & Bit(e)) != 0; }
 
+  /// Equality operator
+  /// @param rhs the other EnumSet to compare this to
+  /// @return true if this EnumSet is equal to rhs
+  inline bool operator==(const EnumSet& rhs) const { return set == rhs.set; }
+
+  /// Inequality operator
+  /// @param rhs the other EnumSet to compare this to
+  /// @return true if this EnumSet is not equal to rhs
+  inline bool operator!=(const EnumSet& rhs) const { return set != rhs.set; }
+
+  /// @return the underlying value for the EnumSet
+  inline uint64_t Value() const { return set; }
+
  private:
   static constexpr uint64_t Bit(Enum value) {
     return static_cast<uint64_t>(1) << static_cast<uint64_t>(value);
@@ -76,4 +90,19 @@
 }  // namespace utils
 }  // namespace tint
 
+namespace std {
+
+/// Custom std::hash specialization for tint::utils::EnumSet<T>
+template <typename T>
+class hash<tint::utils::EnumSet<T>> {
+ public:
+  /// @param e the EnumSet to create a hash for
+  /// @return the hash value
+  inline std::size_t operator()(const tint::utils::EnumSet<T>& e) const {
+    return std::hash<uint64_t>()(e.Value());
+  }
+};
+
+}  // namespace std
+
 #endif  // SRC_UTILS_ENUM_SET_H_
diff --git a/src/utils/enum_set_test.cc b/src/utils/enum_set_test.cc
index d53b834..80c3dcf 100644
--- a/src/utils/enum_set_test.cc
+++ b/src/utils/enum_set_test.cc
@@ -59,6 +59,30 @@
   EXPECT_FALSE(set.Contains(E::C));
 }
 
+TEST(EnumSetTest, Equality) {
+  EXPECT_TRUE(EnumSet<E>(E::A, E::B) == EnumSet<E>(E::A, E::B));
+  EXPECT_FALSE(EnumSet<E>(E::A, E::B) == EnumSet<E>(E::A, E::C));
+}
+
+TEST(EnumSetTest, Inequality) {
+  EXPECT_FALSE(EnumSet<E>(E::A, E::B) != EnumSet<E>(E::A, E::B));
+  EXPECT_TRUE(EnumSet<E>(E::A, E::B) != EnumSet<E>(E::A, E::C));
+}
+
+TEST(EnumSetTest, Hash) {
+  auto hash = [&](EnumSet<E> s) { return std::hash<EnumSet<E>>()(s); };
+  EXPECT_EQ(hash(EnumSet<E>(E::A, E::B)), hash(EnumSet<E>(E::A, E::B)));
+  EXPECT_NE(hash(EnumSet<E>(E::A, E::B)), hash(EnumSet<E>(E::A, E::C)));
+}
+
+TEST(EnumSetTest, Value) {
+  EXPECT_EQ(EnumSet<E>().Value(), 0u);
+  EXPECT_EQ(EnumSet<E>(E::A).Value(), 1u);
+  EXPECT_EQ(EnumSet<E>(E::B).Value(), 2u);
+  EXPECT_EQ(EnumSet<E>(E::C).Value(), 4u);
+  EXPECT_EQ(EnumSet<E>(E::A, E::C).Value(), 5u);
+}
+
 }  // namespace
 }  // namespace utils
 }  // namespace tint
diff --git a/src/utils/get_or_create.h b/src/utils/get_or_create.h
index b5b86ef..dd5e151 100644
--- a/src/utils/get_or_create.h
+++ b/src/utils/get_or_create.h
@@ -27,8 +27,10 @@
 /// @param key the map key of the item to query or add
 /// @param create a callable function-like object with the signature `V()`
 /// @return the value of the item with the given key, or the newly created item
-template <typename K, typename V, typename CREATE, typename H>
-V GetOrCreate(std::unordered_map<K, V, H>& map, K key, CREATE&& create) {
+template <typename K, typename V, typename H, typename C, typename CREATE>
+V GetOrCreate(std::unordered_map<K, V, H, C>& map,
+              const K& key,
+              CREATE&& create) {
   auto it = map.find(key);
   if (it != map.end()) {
     return it->second;
diff --git a/src/utils/hash.h b/src/utils/hash.h
index 44e1e49..bd78942 100644
--- a/src/utils/hash.h
+++ b/src/utils/hash.h
@@ -18,6 +18,7 @@
 #include <stdint.h>
 #include <cstdio>
 #include <functional>
+#include <vector>
 
 namespace tint {
 namespace utils {
@@ -51,6 +52,15 @@
   *hash ^= std::hash<T>()(value) + offset + (*hash << 6) + (*hash >> 2);
 }
 
+// Helper for hashing vectors
+template <typename T>
+void HashCombine(size_t* hash, const std::vector<T>& vector) {
+  HashCombine(hash, vector.size());
+  for (auto& el : vector) {
+    HashCombine(hash, el);
+  }
+}
+
 template <typename T, typename... ARGS>
 void HashCombine(size_t* hash, const T& value, const ARGS&... args) {
   HashCombine(hash, value);
diff --git a/src/utils/hash_test.cc b/src/utils/hash_test.cc
index 177c53b..dd0877d 100644
--- a/src/utils/hash_test.cc
+++ b/src/utils/hash_test.cc
@@ -23,14 +23,25 @@
 namespace {
 
 TEST(HashTests, Basic) {
+  EXPECT_EQ(Hash(123), Hash(123));
   EXPECT_NE(Hash(123), Hash(321));
+  EXPECT_EQ(Hash(123, 456), Hash(123, 456));
+  EXPECT_NE(Hash(123, 456), Hash(456, 123));
   EXPECT_NE(Hash(123, 456), Hash(123));
+  EXPECT_EQ(Hash(123, 456, false), Hash(123, 456, false));
   EXPECT_NE(Hash(123, 456, false), Hash(123, 456));
+  EXPECT_EQ(Hash(std::string("hello")), Hash(std::string("hello")));
   EXPECT_NE(Hash(std::string("hello")), Hash(std::string("world")));
 }
 
-TEST(HashTests, Order) {
-  EXPECT_NE(Hash(123, 456), Hash(456, 123));
+TEST(HashTests, Vector) {
+  EXPECT_EQ(Hash(std::vector<int>({})), Hash(std::vector<int>({})));
+  EXPECT_EQ(Hash(std::vector<int>({1, 2, 3})),
+            Hash(std::vector<int>({1, 2, 3})));
+  EXPECT_NE(Hash(std::vector<int>({1, 2, 3})),
+            Hash(std::vector<int>({1, 2, 4})));
+  EXPECT_NE(Hash(std::vector<int>({1, 2, 3})),
+            Hash(std::vector<int>({1, 2, 3, 4})));
 }
 
 }  // namespace
diff --git a/test/intrinsics/repeated_use.wgsl b/test/intrinsics/repeated_use.wgsl
new file mode 100644
index 0000000..24a8370
--- /dev/null
+++ b/test/intrinsics/repeated_use.wgsl
@@ -0,0 +1,20 @@
+// Check that for backends that generate intrinsic helpers, repeated use of the
+// same intrinsic overload results in single helper being generated.
+[[stage(compute), workgroup_size(1)]]
+fn main() {
+    ignore(isNormal(vec4<f32>()));
+    ignore(isNormal(vec4<f32>(1.)));
+    ignore(isNormal(vec4<f32>(1., 2., 3., 4.)));
+
+    ignore(isNormal(vec3<f32>()));
+    ignore(isNormal(vec3<f32>(1.)));
+    ignore(isNormal(vec3<f32>(1., 2., 3.)));
+
+    ignore(isNormal(vec2<f32>()));
+    ignore(isNormal(vec2<f32>(1.)));
+    ignore(isNormal(vec2<f32>(1., 2.)));
+
+    ignore(isNormal(1.));
+    ignore(isNormal(2.));
+    ignore(isNormal(3.));
+}
diff --git a/test/intrinsics/repeated_use.wgsl.expected.hlsl b/test/intrinsics/repeated_use.wgsl.expected.hlsl
new file mode 100644
index 0000000..c89b9fb
--- /dev/null
+++ b/test/intrinsics/repeated_use.wgsl.expected.hlsl
@@ -0,0 +1,40 @@
+bool4 tint_isNormal(float4 param_0) {
+  uint4 exponent = asuint(param_0) & 0x7f80000;
+  uint4 clamped = clamp(exponent, 0x0080000, 0x7f00000);
+  return clamped == exponent;
+}
+
+bool3 tint_isNormal_1(float3 param_0) {
+  uint3 exponent = asuint(param_0) & 0x7f80000;
+  uint3 clamped = clamp(exponent, 0x0080000, 0x7f00000);
+  return clamped == exponent;
+}
+
+bool2 tint_isNormal_2(float2 param_0) {
+  uint2 exponent = asuint(param_0) & 0x7f80000;
+  uint2 clamped = clamp(exponent, 0x0080000, 0x7f00000);
+  return clamped == exponent;
+}
+
+bool tint_isNormal_3(float param_0) {
+  uint exponent = asuint(param_0) & 0x7f80000;
+  uint clamped = clamp(exponent, 0x0080000, 0x7f00000);
+  return clamped == exponent;
+}
+
+[numthreads(1, 1, 1)]
+void main() {
+  tint_isNormal(float4(0.0f, 0.0f, 0.0f, 0.0f));
+  tint_isNormal(float4((1.0f).xxxx));
+  tint_isNormal(float4(1.0f, 2.0f, 3.0f, 4.0f));
+  tint_isNormal_1(float3(0.0f, 0.0f, 0.0f));
+  tint_isNormal_1(float3((1.0f).xxx));
+  tint_isNormal_1(float3(1.0f, 2.0f, 3.0f));
+  tint_isNormal_2(float2(0.0f, 0.0f));
+  tint_isNormal_2(float2((1.0f).xx));
+  tint_isNormal_2(float2(1.0f, 2.0f));
+  tint_isNormal_3(1.0f);
+  tint_isNormal_3(2.0f);
+  tint_isNormal_3(3.0f);
+  return;
+}
diff --git a/test/intrinsics/repeated_use.wgsl.expected.msl b/test/intrinsics/repeated_use.wgsl.expected.msl
new file mode 100644
index 0000000..16b9698
--- /dev/null
+++ b/test/intrinsics/repeated_use.wgsl.expected.msl
@@ -0,0 +1,19 @@
+#include <metal_stdlib>
+
+using namespace metal;
+kernel void tint_symbol() {
+  (void) isnormal(float4());
+  (void) isnormal(float4(1.0f));
+  (void) isnormal(float4(1.0f, 2.0f, 3.0f, 4.0f));
+  (void) isnormal(float3());
+  (void) isnormal(float3(1.0f));
+  (void) isnormal(float3(1.0f, 2.0f, 3.0f));
+  (void) isnormal(float2());
+  (void) isnormal(float2(1.0f));
+  (void) isnormal(float2(1.0f, 2.0f));
+  (void) isnormal(1.0f);
+  (void) isnormal(2.0f);
+  (void) isnormal(3.0f);
+  return;
+}
+
diff --git a/test/intrinsics/repeated_use.wgsl.expected.spvasm b/test/intrinsics/repeated_use.wgsl.expected.spvasm
new file mode 100644
index 0000000..42acf47
--- /dev/null
+++ b/test/intrinsics/repeated_use.wgsl.expected.spvasm
@@ -0,0 +1,120 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 121
+; Schema: 0
+               OpCapability Shader
+         %12 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %main "main"
+       %void = OpTypeVoid
+          %1 = OpTypeFunction %void
+       %bool = OpTypeBool
+     %v4bool = OpTypeVector %bool 4
+      %float = OpTypeFloat 32
+    %v4float = OpTypeVector %float 4
+         %11 = OpConstantNull %v4float
+       %uint = OpTypeInt 32 0
+%uint_133693440 = OpConstant %uint 133693440
+%uint_524288 = OpConstant %uint 524288
+%uint_133169152 = OpConstant %uint 133169152
+     %v4uint = OpTypeVector %uint 4
+    %float_1 = OpConstant %float 1
+         %27 = OpConstantComposite %v4float %float_1 %float_1 %float_1 %float_1
+    %float_2 = OpConstant %float 2
+    %float_3 = OpConstant %float 3
+    %float_4 = OpConstant %float 4
+         %39 = OpConstantComposite %v4float %float_1 %float_2 %float_3 %float_4
+     %v3bool = OpTypeVector %bool 3
+    %v3float = OpTypeVector %float 3
+         %50 = OpConstantNull %v3float
+     %v3uint = OpTypeVector %uint 3
+         %60 = OpConstantComposite %v3float %float_1 %float_1 %float_1
+         %69 = OpConstantComposite %v3float %float_1 %float_2 %float_3
+     %v2bool = OpTypeVector %bool 2
+    %v2float = OpTypeVector %float 2
+         %80 = OpConstantNull %v2float
+     %v2uint = OpTypeVector %uint 2
+         %90 = OpConstantComposite %v2float %float_1 %float_1
+         %99 = OpConstantComposite %v2float %float_1 %float_2
+       %main = OpFunction %void None %1
+          %4 = OpLabel
+         %18 = OpCompositeConstruct %v4uint %uint_133693440 %uint_133693440 %uint_133693440 %uint_133693440
+         %19 = OpCompositeConstruct %v4uint %uint_524288 %uint_524288 %uint_524288 %uint_524288
+         %20 = OpCompositeConstruct %v4uint %uint_133169152 %uint_133169152 %uint_133169152 %uint_133169152
+         %21 = OpBitcast %v4uint %11
+         %22 = OpBitwiseAnd %v4uint %21 %18
+         %23 = OpExtInst %v4uint %12 UClamp %22 %19 %20
+          %6 = OpIEqual %v4bool %22 %23
+         %28 = OpCompositeConstruct %v4uint %uint_133693440 %uint_133693440 %uint_133693440 %uint_133693440
+         %29 = OpCompositeConstruct %v4uint %uint_524288 %uint_524288 %uint_524288 %uint_524288
+         %30 = OpCompositeConstruct %v4uint %uint_133169152 %uint_133169152 %uint_133169152 %uint_133169152
+         %31 = OpBitcast %v4uint %27
+         %32 = OpBitwiseAnd %v4uint %31 %28
+         %33 = OpExtInst %v4uint %12 UClamp %32 %29 %30
+         %25 = OpIEqual %v4bool %32 %33
+         %40 = OpCompositeConstruct %v4uint %uint_133693440 %uint_133693440 %uint_133693440 %uint_133693440
+         %41 = OpCompositeConstruct %v4uint %uint_524288 %uint_524288 %uint_524288 %uint_524288
+         %42 = OpCompositeConstruct %v4uint %uint_133169152 %uint_133169152 %uint_133169152 %uint_133169152
+         %43 = OpBitcast %v4uint %39
+         %44 = OpBitwiseAnd %v4uint %43 %40
+         %45 = OpExtInst %v4uint %12 UClamp %44 %41 %42
+         %35 = OpIEqual %v4bool %44 %45
+         %52 = OpCompositeConstruct %v3uint %uint_133693440 %uint_133693440 %uint_133693440
+         %53 = OpCompositeConstruct %v3uint %uint_524288 %uint_524288 %uint_524288
+         %54 = OpCompositeConstruct %v3uint %uint_133169152 %uint_133169152 %uint_133169152
+         %55 = OpBitcast %v3uint %50
+         %56 = OpBitwiseAnd %v3uint %55 %52
+         %57 = OpExtInst %v3uint %12 UClamp %56 %53 %54
+         %47 = OpIEqual %v3bool %56 %57
+         %61 = OpCompositeConstruct %v3uint %uint_133693440 %uint_133693440 %uint_133693440
+         %62 = OpCompositeConstruct %v3uint %uint_524288 %uint_524288 %uint_524288
+         %63 = OpCompositeConstruct %v3uint %uint_133169152 %uint_133169152 %uint_133169152
+         %64 = OpBitcast %v3uint %60
+         %65 = OpBitwiseAnd %v3uint %64 %61
+         %66 = OpExtInst %v3uint %12 UClamp %65 %62 %63
+         %59 = OpIEqual %v3bool %65 %66
+         %70 = OpCompositeConstruct %v3uint %uint_133693440 %uint_133693440 %uint_133693440
+         %71 = OpCompositeConstruct %v3uint %uint_524288 %uint_524288 %uint_524288
+         %72 = OpCompositeConstruct %v3uint %uint_133169152 %uint_133169152 %uint_133169152
+         %73 = OpBitcast %v3uint %69
+         %74 = OpBitwiseAnd %v3uint %73 %70
+         %75 = OpExtInst %v3uint %12 UClamp %74 %71 %72
+         %68 = OpIEqual %v3bool %74 %75
+         %82 = OpCompositeConstruct %v2uint %uint_133693440 %uint_133693440
+         %83 = OpCompositeConstruct %v2uint %uint_524288 %uint_524288
+         %84 = OpCompositeConstruct %v2uint %uint_133169152 %uint_133169152
+         %85 = OpBitcast %v2uint %80
+         %86 = OpBitwiseAnd %v2uint %85 %82
+         %87 = OpExtInst %v2uint %12 UClamp %86 %83 %84
+         %77 = OpIEqual %v2bool %86 %87
+         %91 = OpCompositeConstruct %v2uint %uint_133693440 %uint_133693440
+         %92 = OpCompositeConstruct %v2uint %uint_524288 %uint_524288
+         %93 = OpCompositeConstruct %v2uint %uint_133169152 %uint_133169152
+         %94 = OpBitcast %v2uint %90
+         %95 = OpBitwiseAnd %v2uint %94 %91
+         %96 = OpExtInst %v2uint %12 UClamp %95 %92 %93
+         %89 = OpIEqual %v2bool %95 %96
+        %100 = OpCompositeConstruct %v2uint %uint_133693440 %uint_133693440
+        %101 = OpCompositeConstruct %v2uint %uint_524288 %uint_524288
+        %102 = OpCompositeConstruct %v2uint %uint_133169152 %uint_133169152
+        %103 = OpBitcast %v2uint %99
+        %104 = OpBitwiseAnd %v2uint %103 %100
+        %105 = OpExtInst %v2uint %12 UClamp %104 %101 %102
+         %98 = OpIEqual %v2bool %104 %105
+        %108 = OpBitcast %uint %float_1
+        %109 = OpBitwiseAnd %uint %108 %uint_133693440
+        %110 = OpExtInst %uint %12 UClamp %109 %uint_524288 %uint_133169152
+        %107 = OpIEqual %bool %109 %110
+        %113 = OpBitcast %uint %float_2
+        %114 = OpBitwiseAnd %uint %113 %uint_133693440
+        %115 = OpExtInst %uint %12 UClamp %114 %uint_524288 %uint_133169152
+        %112 = OpIEqual %bool %114 %115
+        %118 = OpBitcast %uint %float_3
+        %119 = OpBitwiseAnd %uint %118 %uint_133693440
+        %120 = OpExtInst %uint %12 UClamp %119 %uint_524288 %uint_133169152
+        %117 = OpIEqual %bool %119 %120
+               OpReturn
+               OpFunctionEnd
diff --git a/test/intrinsics/repeated_use.wgsl.expected.wgsl b/test/intrinsics/repeated_use.wgsl.expected.wgsl
new file mode 100644
index 0000000..e708833
--- /dev/null
+++ b/test/intrinsics/repeated_use.wgsl.expected.wgsl
@@ -0,0 +1,15 @@
+[[stage(compute), workgroup_size(1)]]
+fn main() {
+  ignore(isNormal(vec4<f32>()));
+  ignore(isNormal(vec4<f32>(1.0)));
+  ignore(isNormal(vec4<f32>(1.0, 2.0, 3.0, 4.0)));
+  ignore(isNormal(vec3<f32>()));
+  ignore(isNormal(vec3<f32>(1.0)));
+  ignore(isNormal(vec3<f32>(1.0, 2.0, 3.0)));
+  ignore(isNormal(vec2<f32>()));
+  ignore(isNormal(vec2<f32>(1.0)));
+  ignore(isNormal(vec2<f32>(1.0, 2.0)));
+  ignore(isNormal(1.0));
+  ignore(isNormal(2.0));
+  ignore(isNormal(3.0));
+}