tint->dawn: Shuffle source tree in preperation of merging repos

docs/    -> docs/tint/
fuzzers/ -> src/tint/fuzzers/
samples/ -> src/tint/cmd/
src/     -> src/tint/
test/    -> test/tint/

BUG=tint:1418,tint:1433

Change-Id: Id2aa79f989aef3245b80ef4aa37a27ff16cd700b
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/80482
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ryan Harrison <rharrison@chromium.org>
diff --git a/src/tint/utils/concat.h b/src/tint/utils/concat.h
new file mode 100644
index 0000000..c291da9
--- /dev/null
+++ b/src/tint/utils/concat.h
@@ -0,0 +1,22 @@
+
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_CONCAT_H_
+#define SRC_TINT_UTILS_CONCAT_H_
+
+#define TINT_CONCAT_2(a, b) a##b
+#define TINT_CONCAT(a, b) TINT_CONCAT_2(a, b)
+
+#endif  // SRC_TINT_UTILS_CONCAT_H_
diff --git a/src/tint/utils/crc32.h b/src/tint/utils/crc32.h
new file mode 100644
index 0000000..efe2f0e
--- /dev/null
+++ b/src/tint/utils/crc32.h
@@ -0,0 +1,82 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_CRC32_H_
+#define SRC_TINT_UTILS_CRC32_H_
+
+#include <stdint.h>
+
+namespace tint::utils {
+
+/// @returns the CRC32 of the string `s`.
+/// @note this function can be used to calculate the CRC32 of a string literal
+/// at compile time.
+/// @see https://en.wikipedia.org/wiki/Cyclic_redundancy_check#CRC-32_algorithm
+constexpr uint32_t CRC32(const char* s) {
+  constexpr uint32_t kLUT[] = {
+      0,          0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
+      0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
+      0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
+      0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
+      0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
+      0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
+      0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
+      0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
+      0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
+      0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
+      0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
+      0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
+      0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
+      0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
+      0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
+      0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
+      0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
+      0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
+      0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
+      0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
+      0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
+      0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
+      0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
+      0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
+      0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
+      0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
+      0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
+      0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
+      0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
+      0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
+      0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
+      0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
+      0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
+      0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
+      0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
+      0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
+      0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
+      0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
+      0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
+      0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
+      0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
+      0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
+      0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d};
+
+  uint32_t crc = 0xffffffff;
+  for (auto* p = s; *p != '\0'; ++p) {
+    crc =
+        (crc >> 8) ^ kLUT[static_cast<uint8_t>(crc) ^ static_cast<uint8_t>(*p)];
+  }
+  return crc ^ 0xffffffff;
+}
+
+}  // namespace tint::utils
+
+#endif  // SRC_TINT_UTILS_CRC32_H_
diff --git a/src/tint/utils/crc32_test.cc b/src/tint/utils/crc32_test.cc
new file mode 100644
index 0000000..3e80e78
--- /dev/null
+++ b/src/tint/utils/crc32_test.cc
@@ -0,0 +1,35 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/crc32.h"
+
+#include "gtest/gtest.h"
+
+namespace tint::utils {
+namespace {
+
+TEST(CRC32Test, Compiletime) {
+  static_assert(CRC32("") == 0x00000000u);
+  static_assert(CRC32("hello world") == 0x0d4a1185u);
+  static_assert(CRC32("123456789") == 0xcbf43926u);
+}
+
+TEST(CRC32Test, Runtime) {
+  EXPECT_EQ(CRC32(""), 0x00000000u);
+  EXPECT_EQ(CRC32("hello world"), 0x0d4a1185u);
+  EXPECT_EQ(CRC32("123456789"), 0xcbf43926u);
+}
+
+}  // namespace
+}  // namespace tint::utils
diff --git a/src/tint/utils/debugger.cc b/src/tint/utils/debugger.cc
new file mode 100644
index 0000000..aded5b6
--- /dev/null
+++ b/src/tint/utils/debugger.cc
@@ -0,0 +1,63 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/debugger.h"
+
+#ifdef TINT_ENABLE_BREAK_IN_DEBUGGER
+
+#ifdef _MSC_VER
+#include <Windows.h>
+#elif defined(__linux__)
+#include <signal.h>
+#include <fstream>
+#include <string>
+#endif
+
+#ifdef _MSC_VER
+#define TINT_DEBUGGER_BREAK_DEFINED
+void tint::debugger::Break() {
+  if (::IsDebuggerPresent()) {
+    ::DebugBreak();
+  }
+}
+
+#elif defined(__linux__)
+
+#define TINT_DEBUGGER_BREAK_DEFINED
+void tint::debugger::Break() {
+  // A process is being traced (debugged) if "/proc/self/status" contains a
+  // line with "TracerPid: <non-zero-digit>...".
+  bool is_traced = false;
+  std::ifstream fin("/proc/self/status");
+  std::string line;
+  while (!is_traced && std::getline(fin, line)) {
+    const char kPrefix[] = "TracerPid:\t";
+    static constexpr int kPrefixLen = sizeof(kPrefix) - 1;
+    if (line.length() > kPrefixLen &&
+        line.compare(0, kPrefixLen, kPrefix) == 0) {
+      is_traced = line[kPrefixLen] != '0';
+    }
+  }
+
+  if (is_traced) {
+    raise(SIGTRAP);
+  }
+}
+#endif  // platform
+
+#endif  // TINT_ENABLE_BREAK_IN_DEBUGGER
+
+#ifndef TINT_DEBUGGER_BREAK_DEFINED
+void tint::debugger::Break() {}
+#endif
diff --git a/src/tint/utils/debugger.h b/src/tint/utils/debugger.h
new file mode 100644
index 0000000..240a8d9
--- /dev/null
+++ b/src/tint/utils/debugger.h
@@ -0,0 +1,27 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_DEBUGGER_H_
+#define SRC_TINT_UTILS_DEBUGGER_H_
+
+namespace tint::debugger {
+
+/// If debugger is attached and the `TINT_ENABLE_BREAK_IN_DEBUGGER` preprocessor
+/// macro is defined for `debugger.cc`, calling `Break()` will cause the
+/// debugger to break at the call site.
+void Break();
+
+}  // namespace tint::debugger
+
+#endif  // SRC_TINT_UTILS_DEBUGGER_H_
diff --git a/src/tint/utils/defer.h b/src/tint/utils/defer.h
new file mode 100644
index 0000000..4be704e
--- /dev/null
+++ b/src/tint/utils/defer.h
@@ -0,0 +1,63 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_DEFER_H_
+#define SRC_TINT_UTILS_DEFER_H_
+
+#include <utility>
+
+#include "src/tint/utils/concat.h"
+
+namespace tint {
+namespace utils {
+
+/// Defer executes a function or function like object when it is destructed.
+template <typename F>
+class Defer {
+ public:
+  /// Constructor
+  /// @param f the function to call when the Defer is destructed
+  explicit Defer(F&& f) : f_(std::move(f)) {}
+
+  /// Move constructor
+  Defer(Defer&&) = default;
+
+  /// Destructor
+  /// Calls the deferred function
+  ~Defer() { f_(); }
+
+ private:
+  Defer(const Defer&) = delete;
+  Defer& operator=(const Defer&) = delete;
+
+  F f_;
+};
+
+/// Constructor
+/// @param f the function to call when the Defer is destructed
+template <typename F>
+inline Defer<F> MakeDefer(F&& f) {
+  return Defer<F>(std::forward<F>(f));
+}
+
+}  // namespace utils
+}  // namespace tint
+
+/// TINT_DEFER(S) executes the statement(s) `S` when exiting the current lexical
+/// scope.
+#define TINT_DEFER(S)                          \
+  auto TINT_CONCAT(tint_defer_, __COUNTER__) = \
+      ::tint::utils::MakeDefer([&] { S; })
+
+#endif  // SRC_TINT_UTILS_DEFER_H_
diff --git a/src/tint/utils/defer_test.cc b/src/tint/utils/defer_test.cc
new file mode 100644
index 0000000..fe1034c
--- /dev/null
+++ b/src/tint/utils/defer_test.cc
@@ -0,0 +1,44 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/defer.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace utils {
+namespace {
+
+TEST(DeferTest, Basic) {
+  bool deferCalled = false;
+  { TINT_DEFER(deferCalled = true); }
+  ASSERT_TRUE(deferCalled);
+}
+
+TEST(DeferTest, DeferOrder) {
+  int counter = 0;
+  int a = 0, b = 0, c = 0;
+  {
+    TINT_DEFER(a = ++counter);
+    TINT_DEFER(b = ++counter);
+    TINT_DEFER(c = ++counter);
+  }
+  ASSERT_EQ(a, 3);
+  ASSERT_EQ(b, 2);
+  ASSERT_EQ(c, 1);
+}
+
+}  // namespace
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/enum_set.h b/src/tint/utils/enum_set.h
new file mode 100644
index 0000000..a4fc621
--- /dev/null
+++ b/src/tint/utils/enum_set.h
@@ -0,0 +1,258 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_ENUM_SET_H_
+#define SRC_TINT_UTILS_ENUM_SET_H_
+
+#include <cstdint>
+#include <functional>
+#include <ostream>
+#include <type_traits>
+#include <utility>
+
+namespace tint {
+namespace utils {
+
+/// EnumSet is a set of enum values.
+/// @note As the EnumSet is backed by a single uint64_t value, it can only hold
+/// enum values in the range [0 .. 63].
+template <typename ENUM>
+struct EnumSet {
+ public:
+  /// Enum is the enum type this EnumSet wraps
+  using Enum = ENUM;
+
+  /// Constructor. Initializes the EnumSet with zero.
+  constexpr EnumSet() = default;
+
+  /// Copy constructor.
+  /// @param s the set to copy
+  constexpr EnumSet(const EnumSet& s) = default;
+
+  /// Constructor. Initializes the EnumSet with the given values.
+  /// @param values the enumerator values to construct the set with
+  template <typename... VALUES>
+  explicit constexpr EnumSet(VALUES... values) : set(Union(values...)) {}
+
+  /// Copy assignment operator.
+  /// @param set the set to assign to this set
+  /// @return this set so calls can be chained
+  inline EnumSet& operator=(const EnumSet& set) = default;
+
+  /// Copy assignment operator.
+  /// @param e the enum value
+  /// @return this set so calls can be chained
+  inline EnumSet& operator=(Enum e) { return *this = EnumSet{e}; }
+
+  /// Adds all the given values to this set
+  /// @param values the values to add
+  /// @return this set so calls can be chained
+  template <typename... VALUES>
+  inline EnumSet& Add(VALUES... values) {
+    return Add(EnumSet(std::forward<VALUES>(values)...));
+  }
+
+  /// Removes all the given values from this set
+  /// @param values the values to remove
+  /// @return this set so calls can be chained
+  template <typename... VALUES>
+  inline EnumSet& Remove(VALUES... values) {
+    return Remove(EnumSet(std::forward<VALUES>(values)...));
+  }
+
+  /// Adds all of s to this set
+  /// @param s the enum value
+  /// @return this set so calls can be chained
+  inline EnumSet& Add(EnumSet s) { return (*this = *this + s); }
+
+  /// Removes all of s from this set
+  /// @param s the enum value
+  /// @return this set so calls can be chained
+  inline EnumSet& Remove(EnumSet s) { return (*this = *this - s); }
+
+  /// @param e the enum value
+  /// @returns a copy of this set with e added
+  inline EnumSet operator+(Enum e) const {
+    EnumSet out;
+    out.set = set | Bit(e);
+    return out;
+  }
+
+  /// @param e the enum value
+  /// @returns a copy of this set with e removed
+  inline EnumSet operator-(Enum e) const {
+    EnumSet out;
+    out.set = set & ~Bit(e);
+    return out;
+  }
+
+  /// @param s the other set
+  /// @returns the union of this set with s (this ∪ rhs)
+  inline EnumSet operator+(EnumSet s) const {
+    EnumSet out;
+    out.set = set | s.set;
+    return out;
+  }
+
+  /// @param s the other set
+  /// @returns the set of entries found in this but not in s (this \ s)
+  inline EnumSet operator-(EnumSet s) const {
+    EnumSet out;
+    out.set = set & ~s.set;
+    return out;
+  }
+
+  /// @param s the other set
+  /// @returns the intersection of this set with s (this ∩ rhs)
+  inline EnumSet operator&(EnumSet s) const {
+    EnumSet out;
+    out.set = set & s.set;
+    return out;
+  }
+
+  /// @param e the enum value
+  /// @return true if the set contains `e`
+  inline bool Contains(Enum e) const { return (set & Bit(e)) != 0; }
+
+  /// @return true if the set is empty
+  inline bool Empty() const { return set == 0; }
+
+  /// Equality operator
+  /// @param rhs the other EnumSet to compare this to
+  /// @return true if this EnumSet is equal to rhs
+  inline bool operator==(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!=(EnumSet rhs) const { return set != rhs.set; }
+
+  /// Equality operator
+  /// @param rhs the enum to compare this to
+  /// @return true if this EnumSet only contains `rhs`
+  inline bool operator==(Enum rhs) const { return set == Bit(rhs); }
+
+  /// Inequality operator
+  /// @param rhs the enum to compare this to
+  /// @return false if this EnumSet only contains `rhs`
+  inline bool operator!=(Enum rhs) const { return set != Bit(rhs); }
+
+  /// @return the underlying value for the EnumSet
+  inline uint64_t Value() const { return set; }
+
+  /// Iterator provides read-only, unidirectional iterator over the enums of an
+  /// EnumSet.
+  class Iterator {
+    static constexpr int8_t kEnd = 63;
+
+    Iterator(uint64_t s, int8_t b) : set(s), pos(b) {}
+
+    /// Make the constructor accessible to the EnumSet.
+    friend struct EnumSet;
+
+   public:
+    /// @return the Enum value at this point in the iterator
+    Enum operator*() const { return static_cast<Enum>(pos); }
+
+    /// Increments the iterator
+    /// @returns this iterator
+    Iterator& operator++() {
+      while (pos < kEnd) {
+        pos++;
+        if (set & (static_cast<uint64_t>(1) << static_cast<uint64_t>(pos))) {
+          break;
+        }
+      }
+      return *this;
+    }
+
+    /// Equality operator
+    /// @param rhs the Iterator to compare this to
+    /// @return true if the two iterators are equal
+    bool operator==(const Iterator& rhs) const {
+      return set == rhs.set && pos == rhs.pos;
+    }
+
+    /// Inequality operator
+    /// @param rhs the Iterator to compare this to
+    /// @return true if the two iterators are different
+    bool operator!=(const Iterator& rhs) const { return !(*this == rhs); }
+
+   private:
+    const uint64_t set;
+    int8_t pos;
+  };
+
+  /// @returns an read-only iterator to the beginning of the set
+  Iterator begin() {
+    auto it = Iterator{set, -1};
+    ++it;  // Move to first set bit
+    return it;
+  }
+
+  /// @returns an iterator to the beginning of the set
+  Iterator end() { return Iterator{set, Iterator::kEnd}; }
+
+ private:
+  static constexpr uint64_t Bit(Enum value) {
+    return static_cast<uint64_t>(1) << static_cast<uint64_t>(value);
+  }
+
+  static constexpr uint64_t Union() { return 0; }
+
+  template <typename FIRST, typename... VALUES>
+  static constexpr uint64_t Union(FIRST first, VALUES... values) {
+    return Bit(first) | Union(values...);
+  }
+
+  uint64_t set = 0;
+};
+
+/// Writes the EnumSet to the std::ostream.
+/// @param out the std::ostream to write to
+/// @param set the EnumSet to write
+/// @returns out so calls can be chained
+template <typename ENUM>
+inline std::ostream& operator<<(std::ostream& out, EnumSet<ENUM> set) {
+  out << "{";
+  bool first = true;
+  for (auto e : set) {
+    if (!first) {
+      out << ", ";
+    }
+    first = false;
+    out << e;
+  }
+  return out << "}";
+}
+
+}  // 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_TINT_UTILS_ENUM_SET_H_
diff --git a/src/tint/utils/enum_set_test.cc b/src/tint/utils/enum_set_test.cc
new file mode 100644
index 0000000..566701c
--- /dev/null
+++ b/src/tint/utils/enum_set_test.cc
@@ -0,0 +1,244 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/enum_set.h"
+
+#include <sstream>
+#include <vector>
+
+#include "gmock/gmock.h"
+
+namespace tint {
+namespace utils {
+namespace {
+
+using ::testing::ElementsAre;
+
+enum class E { A = 0, B = 3, C = 7 };
+
+std::ostream& operator<<(std::ostream& out, E e) {
+  switch (e) {
+    case E::A:
+      return out << "A";
+    case E::B:
+      return out << "B";
+    case E::C:
+      return out << "C";
+  }
+  return out << "E(" << static_cast<uint32_t>(e) << ")";
+}
+
+TEST(EnumSetTest, ConstructEmpty) {
+  EnumSet<E> set;
+  EXPECT_FALSE(set.Contains(E::A));
+  EXPECT_FALSE(set.Contains(E::B));
+  EXPECT_FALSE(set.Contains(E::C));
+  EXPECT_TRUE(set.Empty());
+}
+
+TEST(EnumSetTest, ConstructWithSingle) {
+  EnumSet<E> set(E::B);
+  EXPECT_FALSE(set.Contains(E::A));
+  EXPECT_TRUE(set.Contains(E::B));
+  EXPECT_FALSE(set.Contains(E::C));
+  EXPECT_FALSE(set.Empty());
+}
+
+TEST(EnumSetTest, ConstructWithMultiple) {
+  EnumSet<E> set(E::A, E::C);
+  EXPECT_TRUE(set.Contains(E::A));
+  EXPECT_FALSE(set.Contains(E::B));
+  EXPECT_TRUE(set.Contains(E::C));
+  EXPECT_FALSE(set.Empty());
+}
+
+TEST(EnumSetTest, AssignSet) {
+  EnumSet<E> set;
+  set = EnumSet<E>(E::A, E::C);
+  EXPECT_TRUE(set.Contains(E::A));
+  EXPECT_FALSE(set.Contains(E::B));
+  EXPECT_TRUE(set.Contains(E::C));
+}
+
+TEST(EnumSetTest, AssignEnum) {
+  EnumSet<E> set(E::A);
+  set = E::B;
+  EXPECT_FALSE(set.Contains(E::A));
+  EXPECT_TRUE(set.Contains(E::B));
+  EXPECT_FALSE(set.Contains(E::C));
+}
+
+TEST(EnumSetTest, AddEnum) {
+  EnumSet<E> set;
+  set.Add(E::B);
+  EXPECT_FALSE(set.Contains(E::A));
+  EXPECT_TRUE(set.Contains(E::B));
+  EXPECT_FALSE(set.Contains(E::C));
+}
+
+TEST(EnumSetTest, RemoveEnum) {
+  EnumSet<E> set(E::A, E::B);
+  set.Remove(E::B);
+  EXPECT_TRUE(set.Contains(E::A));
+  EXPECT_FALSE(set.Contains(E::B));
+  EXPECT_FALSE(set.Contains(E::C));
+}
+
+TEST(EnumSetTest, AddEnums) {
+  EnumSet<E> set;
+  set.Add(E::B, E::C);
+  EXPECT_FALSE(set.Contains(E::A));
+  EXPECT_TRUE(set.Contains(E::B));
+  EXPECT_TRUE(set.Contains(E::C));
+}
+
+TEST(EnumSetTest, RemoveEnums) {
+  EnumSet<E> set(E::A, E::B);
+  set.Remove(E::C, E::B);
+  EXPECT_TRUE(set.Contains(E::A));
+  EXPECT_FALSE(set.Contains(E::B));
+  EXPECT_FALSE(set.Contains(E::C));
+}
+
+TEST(EnumSetTest, AddEnumSet) {
+  EnumSet<E> set;
+  set.Add(EnumSet<E>{E::B, E::C});
+  EXPECT_FALSE(set.Contains(E::A));
+  EXPECT_TRUE(set.Contains(E::B));
+  EXPECT_TRUE(set.Contains(E::C));
+}
+
+TEST(EnumSetTest, RemoveEnumSet) {
+  EnumSet<E> set(E::A, E::B);
+  set.Remove(EnumSet<E>{E::B, E::C});
+  EXPECT_TRUE(set.Contains(E::A));
+  EXPECT_FALSE(set.Contains(E::B));
+  EXPECT_FALSE(set.Contains(E::C));
+}
+
+TEST(EnumSetTest, OperatorPlusEnum) {
+  EnumSet<E> set = EnumSet<E>{E::B} + E::C;
+  EXPECT_FALSE(set.Contains(E::A));
+  EXPECT_TRUE(set.Contains(E::B));
+  EXPECT_TRUE(set.Contains(E::C));
+}
+
+TEST(EnumSetTest, OperatorMinusEnum) {
+  EnumSet<E> set = EnumSet<E>{E::A, E::B} - E::B;
+  EXPECT_TRUE(set.Contains(E::A));
+  EXPECT_FALSE(set.Contains(E::B));
+  EXPECT_FALSE(set.Contains(E::C));
+}
+
+TEST(EnumSetTest, OperatorPlusSet) {
+  EnumSet<E> set = EnumSet<E>{E::B} + EnumSet<E>{E::B, E::C};
+  EXPECT_FALSE(set.Contains(E::A));
+  EXPECT_TRUE(set.Contains(E::B));
+  EXPECT_TRUE(set.Contains(E::C));
+}
+
+TEST(EnumSetTest, OperatorMinusSet) {
+  EnumSet<E> set = EnumSet<E>{E::A, E::B} - EnumSet<E>{E::B, E::C};
+  EXPECT_TRUE(set.Contains(E::A));
+  EXPECT_FALSE(set.Contains(E::B));
+  EXPECT_FALSE(set.Contains(E::C));
+}
+
+TEST(EnumSetTest, OperatorAnd) {
+  EnumSet<E> set = EnumSet<E>{E::A, E::B} & EnumSet<E>{E::B, E::C};
+  EXPECT_FALSE(set.Contains(E::A));
+  EXPECT_TRUE(set.Contains(E::B));
+  EXPECT_FALSE(set.Contains(E::C));
+}
+
+TEST(EnumSetTest, EqualitySet) {
+  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, InequalitySet) {
+  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, EqualityEnum) {
+  EXPECT_TRUE(EnumSet<E>(E::A) == E::A);
+  EXPECT_FALSE(EnumSet<E>(E::B) == E::A);
+  EXPECT_FALSE(EnumSet<E>(E::B) == E::C);
+  EXPECT_FALSE(EnumSet<E>(E::A, E::B) == E::A);
+  EXPECT_FALSE(EnumSet<E>(E::A, E::B) == E::B);
+  EXPECT_FALSE(EnumSet<E>(E::A, E::B) == E::C);
+}
+
+TEST(EnumSetTest, InequalityEnum) {
+  EXPECT_FALSE(EnumSet<E>(E::A) != E::A);
+  EXPECT_TRUE(EnumSet<E>(E::B) != E::A);
+  EXPECT_TRUE(EnumSet<E>(E::B) != E::C);
+  EXPECT_TRUE(EnumSet<E>(E::A, E::B) != E::A);
+  EXPECT_TRUE(EnumSet<E>(E::A, E::B) != E::B);
+  EXPECT_TRUE(EnumSet<E>(E::A, E::B) != 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(), 8u);
+  EXPECT_EQ(EnumSet<E>(E::C).Value(), 128u);
+  EXPECT_EQ(EnumSet<E>(E::A, E::C).Value(), 129u);
+}
+
+TEST(EnumSetTest, Iterator) {
+  auto set = EnumSet<E>(E::C, E::A);
+
+  auto it = set.begin();
+  EXPECT_EQ(*it, E::A);
+  EXPECT_NE(it, set.end());
+  ++it;
+  EXPECT_EQ(*it, E::C);
+  EXPECT_NE(it, set.end());
+  ++it;
+  EXPECT_EQ(it, set.end());
+}
+
+TEST(EnumSetTest, IteratorEmpty) {
+  auto set = EnumSet<E>();
+  EXPECT_EQ(set.begin(), set.end());
+}
+
+TEST(EnumSetTest, Loop) {
+  auto set = EnumSet<E>(E::C, E::A);
+
+  std::vector<E> seen;
+  for (auto e : set) {
+    seen.emplace_back(e);
+  }
+
+  EXPECT_THAT(seen, ElementsAre(E::A, E::C));
+}
+
+TEST(EnumSetTest, Ostream) {
+  std::stringstream ss;
+  ss << EnumSet<E>(E::A, E::C);
+  EXPECT_EQ(ss.str(), "{A, C}");
+}
+
+}  // namespace
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/hash.h b/src/tint/utils/hash.h
new file mode 100644
index 0000000..1158021
--- /dev/null
+++ b/src/tint/utils/hash.h
@@ -0,0 +1,83 @@
+// Copyright 2021 The Tint Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_HASH_H_
+#define SRC_TINT_UTILS_HASH_H_
+
+#include <stdint.h>
+#include <cstdio>
+#include <functional>
+#include <vector>
+
+namespace tint {
+namespace utils {
+namespace detail {
+
+/// Helper for obtaining a seed bias value for HashCombine with a bit-width
+/// dependent on the size of size_t.
+template <int SIZE_OF_SIZE_T>
+struct HashCombineOffset {};
+
+/// Specialization of HashCombineOffset for size_t == 4.
+template <>
+struct HashCombineOffset<4> {
+  /// @returns the seed bias value for HashCombine()
+  static constexpr inline uint32_t value() { return 0x7f4a7c16; }
+};
+
+/// Specialization of HashCombineOffset for size_t == 8.
+template <>
+struct HashCombineOffset<8> {
+  /// @returns the seed bias value for HashCombine()
+  static constexpr inline uint64_t value() { return 0x9e3779b97f4a7c16; }
+};
+
+}  // namespace detail
+
+/// HashCombine "hashes" together an existing hash and hashable values.
+template <typename T>
+void HashCombine(size_t* hash, const T& value) {
+  constexpr size_t offset = detail::HashCombineOffset<sizeof(size_t)>::value();
+  *hash ^= std::hash<T>()(value) + offset + (*hash << 6) + (*hash >> 2);
+}
+
+/// HashCombine "hashes" together an existing hash and hashable values.
+template <typename T>
+void HashCombine(size_t* hash, const std::vector<T>& vector) {
+  HashCombine(hash, vector.size());
+  for (auto& el : vector) {
+    HashCombine(hash, el);
+  }
+}
+
+/// HashCombine "hashes" together an existing hash and hashable values.
+template <typename T, typename... ARGS>
+void HashCombine(size_t* hash, const T& value, const ARGS&... args) {
+  HashCombine(hash, value);
+  HashCombine(hash, args...);
+}
+
+/// @returns a hash of the combined arguments. The returned hash is dependent on
+/// the order of the arguments.
+template <typename... ARGS>
+size_t Hash(const ARGS&... args) {
+  size_t hash = 102931;  // seed with an arbitrary prime
+  HashCombine(&hash, args...);
+  return hash;
+}
+
+}  // namespace utils
+}  // namespace tint
+
+#endif  // SRC_TINT_UTILS_HASH_H_
diff --git a/src/tint/utils/hash_test.cc b/src/tint/utils/hash_test.cc
new file mode 100644
index 0000000..caeba57
--- /dev/null
+++ b/src/tint/utils/hash_test.cc
@@ -0,0 +1,49 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/hash.h"
+
+#include <string>
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace utils {
+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, 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
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/io/command.h b/src/tint/utils/io/command.h
new file mode 100644
index 0000000..dbf587e
--- /dev/null
+++ b/src/tint/utils/io/command.h
@@ -0,0 +1,84 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_IO_COMMAND_H_
+#define SRC_TINT_UTILS_IO_COMMAND_H_
+
+#include <string>
+#include <utility>
+
+namespace tint {
+namespace utils {
+
+/// Command is a helper used by tests for executing a process with a number of
+/// arguments and an optional stdin string, and then collecting and returning
+/// the process's stdout and stderr output as strings.
+class Command {
+ public:
+  /// Output holds the output of the process
+  struct Output {
+    /// stdout from the process
+    std::string out;
+    /// stderr from the process
+    std::string err;
+    /// process error code
+    int error_code = 0;
+  };
+
+  /// Constructor
+  /// @param path path to the executable
+  explicit Command(const std::string& path);
+
+  /// Looks for an executable with the given name in the current working
+  /// directory, and if not found there, in each of the directories in the
+  /// `PATH` environment variable.
+  /// @param executable the executable name
+  /// @returns a Command which will return true for Found() if the executable
+  /// was found.
+  static Command LookPath(const std::string& executable);
+
+  /// @return true if the executable exists at the path provided to the
+  /// constructor
+  bool Found() const;
+
+  /// @returns the path of the command
+  const std::string& Path() const { return path_; }
+
+  /// Invokes the command with the given argument strings, blocking until the
+  /// process has returned.
+  /// @param args the string arguments to pass to the process
+  /// @returns the process output
+  template <typename... ARGS>
+  Output operator()(ARGS... args) const {
+    return Exec({std::forward<ARGS>(args)...});
+  }
+
+  /// Exec invokes the command with the given argument strings, blocking until
+  /// the process has returned.
+  /// @param args the string arguments to pass to the process
+  /// @returns the process output
+  Output Exec(std::initializer_list<std::string> args) const;
+
+  /// @param input the input data to pipe to the process's stdin
+  void SetInput(const std::string& input) { input_ = input; }
+
+ private:
+  std::string const path_;
+  std::string input_;
+};
+
+}  // namespace utils
+}  // namespace tint
+
+#endif  // SRC_TINT_UTILS_IO_COMMAND_H_
diff --git a/src/tint/utils/io/command_other.cc b/src/tint/utils/io/command_other.cc
new file mode 100644
index 0000000..5562e37
--- /dev/null
+++ b/src/tint/utils/io/command_other.cc
@@ -0,0 +1,37 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/io/command.h"
+
+namespace tint {
+namespace utils {
+
+Command::Command(const std::string&) {}
+
+Command Command::LookPath(const std::string&) {
+  return Command("");
+}
+
+bool Command::Found() const {
+  return false;
+}
+
+Command::Output Command::Exec(std::initializer_list<std::string>) const {
+  Output out;
+  out.err = "Command not supported by this target";
+  return out;
+}
+
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/io/command_posix.cc b/src/tint/utils/io/command_posix.cc
new file mode 100644
index 0000000..dc60684
--- /dev/null
+++ b/src/tint/utils/io/command_posix.cc
@@ -0,0 +1,265 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/io/command.h"
+
+#include <sys/poll.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <sstream>
+#include <vector>
+
+namespace tint {
+namespace utils {
+
+namespace {
+
+/// File is a simple wrapper around a POSIX file descriptor
+class File {
+  constexpr static const int kClosed = -1;
+
+ public:
+  /// Constructor
+  File() : handle_(kClosed) {}
+
+  /// Constructor
+  explicit File(int handle) : handle_(handle) {}
+
+  /// Destructor
+  ~File() { Close(); }
+
+  /// Move assignment operator
+  File& operator=(File&& rhs) {
+    Close();
+    handle_ = rhs.handle_;
+    rhs.handle_ = kClosed;
+    return *this;
+  }
+
+  /// Closes the file (if it wasn't already closed)
+  void Close() {
+    if (handle_ != kClosed) {
+      close(handle_);
+    }
+    handle_ = kClosed;
+  }
+
+  /// @returns the file handle
+  operator int() { return handle_; }
+
+  /// @returns true if the file is not closed
+  operator bool() { return handle_ != kClosed; }
+
+ private:
+  File(const File&) = delete;
+  File& operator=(const File&) = delete;
+
+  int handle_ = kClosed;
+};
+
+/// Pipe is a simple wrapper around a POSIX pipe() function
+class Pipe {
+ public:
+  /// Constructs the pipe
+  Pipe() {
+    int pipes[2] = {};
+    if (pipe(pipes) == 0) {
+      read = File(pipes[0]);
+      write = File(pipes[1]);
+    }
+  }
+
+  /// Closes both the read and write files (if they're not already closed)
+  void Close() {
+    read.Close();
+    write.Close();
+  }
+
+  /// @returns true if the pipe has an open read or write file
+  operator bool() { return read || write; }
+
+  /// The reader end of the pipe
+  File read;
+
+  /// The writer end of the pipe
+  File write;
+};
+
+bool ExecutableExists(const std::string& path) {
+  struct stat s {};
+  if (stat(path.c_str(), &s) != 0) {
+    return false;
+  }
+  return s.st_mode & S_IXUSR;
+}
+
+std::string FindExecutable(const std::string& name) {
+  if (ExecutableExists(name)) {
+    return name;
+  }
+  if (name.find("/") == std::string::npos) {
+    auto* path_env = getenv("PATH");
+    if (!path_env) {
+      return "";
+    }
+    std::istringstream path{path_env};
+    std::string dir;
+    while (getline(path, dir, ':')) {
+      auto test = dir + "/" + name;
+      if (ExecutableExists(test)) {
+        return test;
+      }
+    }
+  }
+  return "";
+}
+
+}  // namespace
+
+Command::Command(const std::string& path) : path_(path) {}
+
+Command Command::LookPath(const std::string& executable) {
+  return Command(FindExecutable(executable));
+}
+
+bool Command::Found() const {
+  return ExecutableExists(path_);
+}
+
+Command::Output Command::Exec(
+    std::initializer_list<std::string> arguments) const {
+  if (!Found()) {
+    Output out;
+    out.err = "Executable not found";
+    return out;
+  }
+
+  // Pipes used for piping std[in,out,err] to / from the target process.
+  Pipe stdin_pipe;
+  Pipe stdout_pipe;
+  Pipe stderr_pipe;
+
+  if (!stdin_pipe || !stdout_pipe || !stderr_pipe) {
+    Output output;
+    output.err = "Command::Exec(): Failed to create pipes";
+    return output;
+  }
+
+  // execv() and friends replace the current process image with the target
+  // process image. To keep process that called this function going, we need to
+  // fork() this process into a child and parent process.
+  //
+  // The child process is responsible for hooking up the pipes to
+  // std[in,out,err]_pipes to STD[IN,OUT,ERR]_FILENO and then calling execv() to
+  // run the target command.
+  //
+  // The parent process is responsible for feeding any input to the stdin_pipe
+  // and collectting output from the std[out,err]_pipes.
+
+  int child_id = fork();
+  if (child_id < 0) {
+    Output output;
+    output.err = "Command::Exec(): fork() failed";
+    return output;
+  }
+
+  if (child_id > 0) {
+    // fork() - parent
+
+    // Close the stdout and stderr writer pipes.
+    // This is required for getting poll() POLLHUP events.
+    stdout_pipe.write.Close();
+    stderr_pipe.write.Close();
+
+    // Write the input to the child process
+    if (!input_.empty()) {
+      ssize_t n = write(stdin_pipe.write, input_.data(), input_.size());
+      if (n != static_cast<ssize_t>(input_.size())) {
+        Output output;
+        output.err = "Command::Exec(): write() for stdin failed";
+        return output;
+      }
+    }
+    stdin_pipe.write.Close();
+
+    // Accumulate the stdout and stderr output from the child process
+    pollfd poll_fds[2];
+    poll_fds[0].fd = stdout_pipe.read;
+    poll_fds[0].events = POLLIN;
+    poll_fds[1].fd = stderr_pipe.read;
+    poll_fds[1].events = POLLIN;
+
+    Output output;
+    bool stdout_open = true;
+    bool stderr_open = true;
+    while (stdout_open || stderr_open) {
+      if (poll(poll_fds, 2, -1) < 0) {
+        break;
+      }
+      char buf[256];
+      if (poll_fds[0].revents & POLLIN) {
+        auto n = read(stdout_pipe.read, buf, sizeof(buf));
+        if (n > 0) {
+          output.out += std::string(buf, buf + n);
+        }
+      }
+      if (poll_fds[0].revents & POLLHUP) {
+        stdout_open = false;
+      }
+      if (poll_fds[1].revents & POLLIN) {
+        auto n = read(stderr_pipe.read, buf, sizeof(buf));
+        if (n > 0) {
+          output.err += std::string(buf, buf + n);
+        }
+      }
+      if (poll_fds[1].revents & POLLHUP) {
+        stderr_open = false;
+      }
+    }
+
+    // Get the resulting error code
+    waitpid(child_id, &output.error_code, 0);
+
+    return output;
+  } else {
+    // fork() - child
+
+    // Redirect the stdin, stdout, stderr pipes for the execv process
+    if ((dup2(stdin_pipe.read, STDIN_FILENO) == -1) ||
+        (dup2(stdout_pipe.write, STDOUT_FILENO) == -1) ||
+        (dup2(stderr_pipe.write, STDERR_FILENO) == -1)) {
+      fprintf(stderr, "Command::Exec(): Failed to redirect pipes");
+      exit(errno);
+    }
+
+    // Close the pipes, once redirected above, we're now done with them.
+    stdin_pipe.Close();
+    stdout_pipe.Close();
+    stderr_pipe.Close();
+
+    // Run target executable
+    std::vector<const char*> args;
+    args.emplace_back(path_.c_str());
+    for (auto& arg : arguments) {
+      args.emplace_back(arg.c_str());
+    }
+    args.emplace_back(nullptr);
+    auto res = execv(path_.c_str(), const_cast<char* const*>(args.data()));
+    exit(res);
+  }
+}
+
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/io/command_test.cc b/src/tint/utils/io/command_test.cc
new file mode 100644
index 0000000..f76dcae
--- /dev/null
+++ b/src/tint/utils/io/command_test.cc
@@ -0,0 +1,92 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/io/command.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace utils {
+namespace {
+
+#ifdef _WIN32
+
+TEST(CommandTest, Echo) {
+  auto cmd = Command::LookPath("cmd");
+  if (!cmd.Found()) {
+    GTEST_SKIP() << "cmd not found on PATH";
+  }
+
+  auto res = cmd("/C", "echo", "hello world");
+  EXPECT_EQ(res.error_code, 0);
+  EXPECT_EQ(res.out, "hello world\r\n");
+  EXPECT_EQ(res.err, "");
+}
+
+#else
+
+TEST(CommandTest, Echo) {
+  auto cmd = Command::LookPath("echo");
+  if (!cmd.Found()) {
+    GTEST_SKIP() << "echo not found on PATH";
+  }
+
+  auto res = cmd("hello world");
+  EXPECT_EQ(res.error_code, 0);
+  EXPECT_EQ(res.out, "hello world\n");
+  EXPECT_EQ(res.err, "");
+}
+
+TEST(CommandTest, Cat) {
+  auto cmd = Command::LookPath("cat");
+  if (!cmd.Found()) {
+    GTEST_SKIP() << "cat not found on PATH";
+  }
+
+  cmd.SetInput("hello world");
+  auto res = cmd();
+  EXPECT_EQ(res.error_code, 0);
+  EXPECT_EQ(res.out, "hello world");
+  EXPECT_EQ(res.err, "");
+}
+
+TEST(CommandTest, True) {
+  auto cmd = Command::LookPath("true");
+  if (!cmd.Found()) {
+    GTEST_SKIP() << "true not found on PATH";
+  }
+
+  auto res = cmd();
+  EXPECT_EQ(res.error_code, 0);
+  EXPECT_EQ(res.out, "");
+  EXPECT_EQ(res.err, "");
+}
+
+TEST(CommandTest, False) {
+  auto cmd = Command::LookPath("false");
+  if (!cmd.Found()) {
+    GTEST_SKIP() << "false not found on PATH";
+  }
+
+  auto res = cmd();
+  EXPECT_NE(res.error_code, 0);
+  EXPECT_EQ(res.out, "");
+  EXPECT_EQ(res.err, "");
+}
+
+#endif
+
+}  // namespace
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/io/command_windows.cc b/src/tint/utils/io/command_windows.cc
new file mode 100644
index 0000000..576fc4c
--- /dev/null
+++ b/src/tint/utils/io/command_windows.cc
@@ -0,0 +1,249 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/io/command.h"
+
+#define WIN32_LEAN_AND_MEAN 1
+#include <Windows.h>
+#include <sstream>
+#include <string>
+
+namespace tint {
+namespace utils {
+
+namespace {
+
+/// Handle is a simple wrapper around the Win32 HANDLE
+class Handle {
+ public:
+  /// Constructor
+  Handle() : handle_(nullptr) {}
+
+  /// Constructor
+  explicit Handle(HANDLE handle) : handle_(handle) {}
+
+  /// Destructor
+  ~Handle() { Close(); }
+
+  /// Move assignment operator
+  Handle& operator=(Handle&& rhs) {
+    Close();
+    handle_ = rhs.handle_;
+    rhs.handle_ = nullptr;
+    return *this;
+  }
+
+  /// Closes the handle (if it wasn't already closed)
+  void Close() {
+    if (handle_) {
+      CloseHandle(handle_);
+    }
+    handle_ = nullptr;
+  }
+
+  /// @returns the handle
+  operator HANDLE() { return handle_; }
+
+  /// @returns true if the handle is not invalid
+  operator bool() { return handle_ != nullptr; }
+
+ private:
+  Handle(const Handle&) = delete;
+  Handle& operator=(const Handle&) = delete;
+
+  HANDLE handle_ = nullptr;
+};
+
+/// Pipe is a simple wrapper around a Win32 CreatePipe() function
+class Pipe {
+ public:
+  /// Constructs the pipe
+  explicit Pipe(bool for_read) {
+    SECURITY_ATTRIBUTES sa;
+    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
+    sa.bInheritHandle = TRUE;
+    sa.lpSecurityDescriptor = nullptr;
+
+    HANDLE hread;
+    HANDLE hwrite;
+    if (CreatePipe(&hread, &hwrite, &sa, 0)) {
+      read = Handle(hread);
+      write = Handle(hwrite);
+      // Ensure the read handle to the pipe is not inherited
+      if (!SetHandleInformation(for_read ? read : write, HANDLE_FLAG_INHERIT,
+                                0)) {
+        read.Close();
+        write.Close();
+      }
+    }
+  }
+
+  /// @returns true if the pipe has an open read or write file
+  operator bool() { return read || write; }
+
+  /// The reader end of the pipe
+  Handle read;
+
+  /// The writer end of the pipe
+  Handle write;
+};
+
+bool ExecutableExists(const std::string& path) {
+  DWORD type = 0;
+  return GetBinaryTypeA(path.c_str(), &type);
+}
+
+std::string FindExecutable(const std::string& name) {
+  if (ExecutableExists(name)) {
+    return name;
+  }
+  if (ExecutableExists(name + ".exe")) {
+    return name + ".exe";
+  }
+  if (name.find("/") == std::string::npos &&
+      name.find("\\") == std::string::npos) {
+    char* path_env = nullptr;
+    size_t path_env_len = 0;
+    if (_dupenv_s(&path_env, &path_env_len, "PATH")) {
+      return "";
+    }
+    std::istringstream path{path_env};
+    free(path_env);
+    std::string dir;
+    while (getline(path, dir, ';')) {
+      auto test = dir + "\\" + name;
+      if (ExecutableExists(test)) {
+        return test;
+      }
+      if (ExecutableExists(test + ".exe")) {
+        return test + ".exe";
+      }
+    }
+  }
+  return "";
+}
+
+}  // namespace
+
+Command::Command(const std::string& path) : path_(path) {}
+
+Command Command::LookPath(const std::string& executable) {
+  return Command(FindExecutable(executable));
+}
+
+bool Command::Found() const {
+  return ExecutableExists(path_);
+}
+
+Command::Output Command::Exec(
+    std::initializer_list<std::string> arguments) const {
+  Pipe stdout_pipe(true);
+  Pipe stderr_pipe(true);
+  Pipe stdin_pipe(false);
+  if (!stdin_pipe || !stdout_pipe || !stderr_pipe) {
+    Output output;
+    output.err = "Command::Exec(): Failed to create pipes";
+    return output;
+  }
+
+  if (!input_.empty()) {
+    if (!WriteFile(stdin_pipe.write, input_.data(), input_.size(), nullptr,
+                   nullptr)) {
+      Output output;
+      output.err = "Command::Exec() Failed to write stdin";
+      return output;
+    }
+  }
+  stdin_pipe.write.Close();
+
+  STARTUPINFOA si{};
+  si.cb = sizeof(si);
+  si.dwFlags |= STARTF_USESTDHANDLES;
+  si.hStdOutput = stdout_pipe.write;
+  si.hStdError = stderr_pipe.write;
+  si.hStdInput = stdin_pipe.read;
+
+  std::stringstream args;
+  args << path_;
+  for (auto& arg : arguments) {
+    args << " " << arg;
+  }
+
+  PROCESS_INFORMATION pi{};
+  if (!CreateProcessA(nullptr,  // No module name (use command line)
+                      const_cast<LPSTR>(args.str().c_str()),  // Command line
+                      nullptr,  // Process handle not inheritable
+                      nullptr,  // Thread handle not inheritable
+                      TRUE,     // Handles are inherited
+                      0,        // No creation flags
+                      nullptr,  // Use parent's environment block
+                      nullptr,  // Use parent's starting directory
+                      &si,      // Pointer to STARTUPINFO structure
+                      &pi)) {   // Pointer to PROCESS_INFORMATION structure
+    Output out;
+    out.err = "Command::Exec() CreateProcess() failed";
+    return out;
+  }
+
+  stdin_pipe.read.Close();
+  stdout_pipe.write.Close();
+  stderr_pipe.write.Close();
+
+  struct StreamReadThreadArgs {
+    HANDLE stream;
+    std::string output;
+  };
+
+  auto stream_read_thread = [](LPVOID user) -> DWORD {
+    auto* thread_args = reinterpret_cast<StreamReadThreadArgs*>(user);
+    DWORD n = 0;
+    char buf[256];
+    while (ReadFile(thread_args->stream, buf, sizeof(buf), &n, NULL)) {
+      auto s = std::string(buf, buf + n);
+      thread_args->output += std::string(buf, buf + n);
+    }
+    return 0;
+  };
+
+  StreamReadThreadArgs stdout_read_args{stdout_pipe.read, {}};
+  auto* stdout_read_thread = ::CreateThread(nullptr, 0, stream_read_thread,
+                                            &stdout_read_args, 0, nullptr);
+
+  StreamReadThreadArgs stderr_read_args{stderr_pipe.read, {}};
+  auto* stderr_read_thread = ::CreateThread(nullptr, 0, stream_read_thread,
+                                            &stderr_read_args, 0, nullptr);
+
+  HANDLE handles[] = {pi.hProcess, stdout_read_thread, stderr_read_thread};
+  constexpr DWORD num_handles = sizeof(handles) / sizeof(handles[0]);
+
+  Output output;
+
+  auto res = WaitForMultipleObjects(num_handles, handles, /* wait_all = */ TRUE,
+                                    INFINITE);
+  if (res >= WAIT_OBJECT_0 && res < WAIT_OBJECT_0 + num_handles) {
+    output.out = stdout_read_args.output;
+    output.err = stderr_read_args.output;
+    DWORD exit_code = 0;
+    GetExitCodeProcess(pi.hProcess, &exit_code);
+    output.error_code = static_cast<int>(exit_code);
+  } else {
+    output.err = "Command::Exec() WaitForMultipleObjects() returned " +
+                 std::to_string(res);
+  }
+
+  return output;
+}
+
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/io/tmpfile.h b/src/tint/utils/io/tmpfile.h
new file mode 100644
index 0000000..07f48af
--- /dev/null
+++ b/src/tint/utils/io/tmpfile.h
@@ -0,0 +1,76 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_IO_TMPFILE_H_
+#define SRC_TINT_UTILS_IO_TMPFILE_H_
+
+#include <sstream>
+#include <string>
+
+namespace tint {
+namespace utils {
+
+/// TmpFile constructs a temporary file that can be written to, and is
+/// automatically deleted on destruction.
+class TmpFile {
+ public:
+  /// Constructor.
+  /// Creates a new temporary file which can be written to.
+  /// The temporary file will be automatically deleted on destruction.
+  /// @param extension optional file extension to use with the file. The file
+  /// have no extension by default.
+  explicit TmpFile(std::string extension = "");
+
+  /// Destructor.
+  /// Deletes the temporary file.
+  ~TmpFile();
+
+  /// @return true if the temporary file was successfully created.
+  operator bool() { return !path_.empty(); }
+
+  /// @return the path to the temporary file
+  std::string Path() const { return path_; }
+
+  /// Opens the temporary file and appends |size| bytes from |data| to the end
+  /// of the temporary file. The temporary file is closed again before
+  /// returning, allowing other processes to open the file on operating systems
+  /// that require exclusive ownership of opened files.
+  /// @param data the data to write to the end of the file
+  /// @param size the number of bytes to write from data
+  /// @returns true on success, otherwise false
+  bool Append(const void* data, size_t size) const;
+
+  /// Appends the argument to the end of the file.
+  /// @param data the data to write to the end of the file
+  /// @return a reference to this TmpFile
+  template <typename T>
+  inline TmpFile& operator<<(T&& data) {
+    std::stringstream ss;
+    ss << data;
+    std::string str = ss.str();
+    Append(str.data(), str.size());
+    return *this;
+  }
+
+ private:
+  TmpFile(const TmpFile&) = delete;
+  TmpFile& operator=(const TmpFile&) = delete;
+
+  std::string path_;
+};
+
+}  // namespace utils
+}  // namespace tint
+
+#endif  // SRC_TINT_UTILS_IO_TMPFILE_H_
diff --git a/src/tint/utils/io/tmpfile_other.cc b/src/tint/utils/io/tmpfile_other.cc
new file mode 100644
index 0000000..b4c1a18
--- /dev/null
+++ b/src/tint/utils/io/tmpfile_other.cc
@@ -0,0 +1,29 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/io/tmpfile.h"
+
+namespace tint {
+namespace utils {
+
+TmpFile::TmpFile(std::string) {}
+
+TmpFile::~TmpFile() = default;
+
+bool TmpFile::Append(const void*, size_t) const {
+  return false;
+}
+
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/io/tmpfile_posix.cc b/src/tint/utils/io/tmpfile_posix.cc
new file mode 100644
index 0000000..db9ee47
--- /dev/null
+++ b/src/tint/utils/io/tmpfile_posix.cc
@@ -0,0 +1,70 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/io/tmpfile.h"
+
+#include <unistd.h>
+#include <limits>
+
+#include "src/tint/debug.h"
+
+namespace tint {
+namespace utils {
+
+namespace {
+
+std::string TmpFilePath(std::string ext) {
+  char const* dir = getenv("TMPDIR");
+  if (dir == nullptr) {
+    dir = "/tmp";
+  }
+
+  // mkstemps requires an `int` for the file extension name but STL represents
+  // size_t. Pre-C++20 there the behavior for unsigned-to-signed conversion
+  // (when the source value exceeds the representable range) is implementation
+  // defined. While such a large file extension is unlikely in practice, we
+  // enforce this here at runtime.
+  TINT_ASSERT(Utils, ext.length() <=
+                         static_cast<size_t>(std::numeric_limits<int>::max()));
+  std::string name = std::string(dir) + "/tint_XXXXXX" + ext;
+  int file = mkstemps(&name[0], static_cast<int>(ext.length()));
+  if (file != -1) {
+    close(file);
+    return name;
+  }
+  return "";
+}
+
+}  // namespace
+
+TmpFile::TmpFile(std::string extension)
+    : path_(TmpFilePath(std::move(extension))) {}
+
+TmpFile::~TmpFile() {
+  if (!path_.empty()) {
+    remove(path_.c_str());
+  }
+}
+
+bool TmpFile::Append(const void* data, size_t size) const {
+  if (auto* file = fopen(path_.c_str(), "ab")) {
+    fwrite(data, size, 1, file);
+    fclose(file);
+    return true;
+  }
+  return false;
+}
+
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/io/tmpfile_test.cc b/src/tint/utils/io/tmpfile_test.cc
new file mode 100644
index 0000000..abd9511
--- /dev/null
+++ b/src/tint/utils/io/tmpfile_test.cc
@@ -0,0 +1,90 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/io/tmpfile.h"
+
+#include <fstream>
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace utils {
+namespace {
+
+TEST(TmpFileTest, WriteReadAppendDelete) {
+  std::string path;
+  {
+    TmpFile tmp;
+    if (!tmp) {
+      GTEST_SKIP() << "Unable to create a temporary file";
+    }
+
+    path = tmp.Path();
+
+    // Write a string to the temporary file
+    tmp << "hello world\n";
+
+    // Check the content of the file
+    {
+      std::ifstream file(path);
+      ASSERT_TRUE(file);
+      std::string line;
+      EXPECT_TRUE(std::getline(file, line));
+      EXPECT_EQ(line, "hello world");
+      EXPECT_FALSE(std::getline(file, line));
+    }
+
+    // Write some more content to the file
+    tmp << 42;
+
+    // Check the content of the file again
+    {
+      std::ifstream file(path);
+      ASSERT_TRUE(file);
+      std::string line;
+      EXPECT_TRUE(std::getline(file, line));
+      EXPECT_EQ(line, "hello world");
+      EXPECT_TRUE(std::getline(file, line));
+      EXPECT_EQ(line, "42");
+      EXPECT_FALSE(std::getline(file, line));
+    }
+  }
+
+  // Check the file has been deleted when it fell out of scope
+  std::ifstream file(path);
+  ASSERT_FALSE(file);
+}
+
+TEST(TmpFileTest, FileExtension) {
+  const std::string kExt = ".foo";
+  std::string path;
+  {
+    TmpFile tmp(kExt);
+    if (!tmp) {
+      GTEST_SKIP() << "Unable create a temporary file";
+    }
+    path = tmp.Path();
+  }
+
+  ASSERT_GT(path.length(), kExt.length());
+  EXPECT_EQ(kExt, path.substr(path.length() - kExt.length()));
+
+  // Check the file has been deleted when it fell out of scope
+  std::ifstream file(path);
+  ASSERT_FALSE(file);
+}
+
+}  // namespace
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/io/tmpfile_windows.cc b/src/tint/utils/io/tmpfile_windows.cc
new file mode 100644
index 0000000..2b57b68
--- /dev/null
+++ b/src/tint/utils/io/tmpfile_windows.cc
@@ -0,0 +1,63 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/io/tmpfile.h"
+
+#include <stdio.h>
+#include <cstdio>
+
+namespace tint {
+namespace utils {
+
+namespace {
+
+std::string TmpFilePath(const std::string& ext) {
+  char name[L_tmpnam];
+  // As we're adding an extension, to ensure the file is really unique, try
+  // creating it, failing if it already exists.
+  while (tmpnam_s(name, L_tmpnam - 1) == 0) {
+    std::string name_with_ext = std::string(name) + ext;
+    FILE* f = nullptr;
+    // The "x" arg forces the function to fail if the file already exists.
+    fopen_s(&f, name_with_ext.c_str(), "wbx");
+    if (f) {
+      fclose(f);
+      return name_with_ext;
+    }
+  }
+  return {};
+}
+
+}  // namespace
+
+TmpFile::TmpFile(std::string ext) : path_(TmpFilePath(ext)) {}
+
+TmpFile::~TmpFile() {
+  if (!path_.empty()) {
+    remove(path_.c_str());
+  }
+}
+
+bool TmpFile::Append(const void* data, size_t size) const {
+  FILE* file = nullptr;
+  if (fopen_s(&file, path_.c_str(), "ab") != 0) {
+    return false;
+  }
+  fwrite(data, size, 1, file);
+  fclose(file);
+  return true;
+}
+
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/map.h b/src/tint/utils/map.h
new file mode 100644
index 0000000..0f60d13
--- /dev/null
+++ b/src/tint/utils/map.h
@@ -0,0 +1,62 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_MAP_H_
+#define SRC_TINT_UTILS_MAP_H_
+
+#include <unordered_map>
+
+namespace tint {
+namespace utils {
+
+/// Lookup is a utility function for fetching a value from an unordered map if
+/// it exists, otherwise returning the `if_missing` argument.
+/// @param map the unordered_map
+/// @param key the map key of the item to query
+/// @param if_missing the value to return if the map does not contain the given
+/// key. Defaults to the zero-initializer for the value type.
+/// @return the map item value, or `if_missing` if the map does not contain the
+/// given key
+template <typename K, typename V, typename H, typename C, typename KV = K>
+V Lookup(const std::unordered_map<K, V, H, C>& map,
+         const KV& key,
+         const V& if_missing = {}) {
+  auto it = map.find(key);
+  return it != map.end() ? it->second : if_missing;
+}
+
+/// GetOrCreate is a utility function for lazily adding to an unordered map.
+/// If the map already contains the key `key` then this is returned, otherwise
+/// `create()` is called and the result is added to the map and is returned.
+/// @param map the unordered_map
+/// @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 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;
+  }
+  V value = create();
+  map.emplace(key, value);
+  return value;
+}
+
+}  // namespace utils
+}  // namespace tint
+
+#endif  // SRC_TINT_UTILS_MAP_H_
diff --git a/src/tint/utils/map_test.cc b/src/tint/utils/map_test.cc
new file mode 100644
index 0000000..a1e8dec
--- /dev/null
+++ b/src/tint/utils/map_test.cc
@@ -0,0 +1,58 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/map.h"
+
+#include <unordered_map>
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace utils {
+namespace {
+
+TEST(Lookup, Test) {
+  std::unordered_map<int, int> map;
+  map.emplace(10, 1);
+  EXPECT_EQ(Lookup(map, 10, 0), 1);    // exists, with if_missing
+  EXPECT_EQ(Lookup(map, 10), 1);       // exists, without if_missing
+  EXPECT_EQ(Lookup(map, 20, 50), 50);  // missing, with if_missing
+  EXPECT_EQ(Lookup(map, 20), 0);       // missing, without if_missing
+}
+
+TEST(GetOrCreateTest, NewKey) {
+  std::unordered_map<int, int> map;
+  EXPECT_EQ(GetOrCreate(map, 1, [&] { return 2; }), 2);
+  EXPECT_EQ(map.size(), 1u);
+  EXPECT_EQ(map[1], 2);
+}
+
+TEST(GetOrCreateTest, ExistingKey) {
+  std::unordered_map<int, int> map;
+  map[1] = 2;
+  bool called = false;
+  EXPECT_EQ(GetOrCreate(map, 1,
+                        [&] {
+                          called = true;
+                          return -2;
+                        }),
+            2);
+  EXPECT_EQ(called, false);
+  EXPECT_EQ(map.size(), 1u);
+  EXPECT_EQ(map[1], 2);
+}
+
+}  // namespace
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/math.h b/src/tint/utils/math.h
new file mode 100644
index 0000000..155e39f
--- /dev/null
+++ b/src/tint/utils/math.h
@@ -0,0 +1,57 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_MATH_H_
+#define SRC_TINT_UTILS_MATH_H_
+
+#include <sstream>
+#include <string>
+#include <type_traits>
+
+namespace tint {
+namespace utils {
+
+/// @param alignment the next multiple to round `value` to
+/// @param value the value to round to the next multiple of `alignment`
+/// @return `value` rounded to the next multiple of `alignment`
+/// @note `alignment` must be positive. An alignment of zero will cause a DBZ.
+template <typename T>
+inline T RoundUp(T alignment, T value) {
+  return ((value + alignment - 1) / alignment) * alignment;
+}
+
+/// @param value the value to check whether it is a power-of-two
+/// @returns true if `value` is a power-of-two
+/// @note `value` must be positive if `T` is signed
+template <typename T>
+inline bool IsPowerOfTwo(T value) {
+  return (value & (value - 1)) == 0;
+}
+
+/// @param value the input value
+/// @returns the largest power of two that `value` is a multiple of
+template <typename T>
+inline std::enable_if_t<std::is_unsigned<T>::value, T> MaxAlignOf(T value) {
+  T pot = 1;
+  while (value && ((value & 1u) == 0)) {
+    pot <<= 1;
+    value >>= 1;
+  }
+  return pot;
+}
+
+}  // namespace utils
+}  // namespace tint
+
+#endif  // SRC_TINT_UTILS_MATH_H_
diff --git a/src/tint/utils/math_test.cc b/src/tint/utils/math_test.cc
new file mode 100644
index 0000000..057f142
--- /dev/null
+++ b/src/tint/utils/math_test.cc
@@ -0,0 +1,83 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/math.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace utils {
+namespace {
+
+TEST(MathTests, RoundUp) {
+  EXPECT_EQ(RoundUp(1, 0), 0);
+  EXPECT_EQ(RoundUp(1, 1), 1);
+  EXPECT_EQ(RoundUp(1, 2), 2);
+
+  EXPECT_EQ(RoundUp(1, 1), 1);
+  EXPECT_EQ(RoundUp(2, 1), 2);
+  EXPECT_EQ(RoundUp(3, 1), 3);
+  EXPECT_EQ(RoundUp(4, 1), 4);
+
+  EXPECT_EQ(RoundUp(1, 2), 2);
+  EXPECT_EQ(RoundUp(2, 2), 2);
+  EXPECT_EQ(RoundUp(3, 2), 3);
+  EXPECT_EQ(RoundUp(4, 2), 4);
+
+  EXPECT_EQ(RoundUp(1, 3), 3);
+  EXPECT_EQ(RoundUp(2, 3), 4);
+  EXPECT_EQ(RoundUp(3, 3), 3);
+  EXPECT_EQ(RoundUp(4, 3), 4);
+
+  EXPECT_EQ(RoundUp(1, 4), 4);
+  EXPECT_EQ(RoundUp(2, 4), 4);
+  EXPECT_EQ(RoundUp(3, 4), 6);
+  EXPECT_EQ(RoundUp(4, 4), 4);
+}
+
+TEST(MathTests, IsPowerOfTwo) {
+  EXPECT_EQ(IsPowerOfTwo(1), true);
+  EXPECT_EQ(IsPowerOfTwo(2), true);
+  EXPECT_EQ(IsPowerOfTwo(3), false);
+  EXPECT_EQ(IsPowerOfTwo(4), true);
+  EXPECT_EQ(IsPowerOfTwo(5), false);
+  EXPECT_EQ(IsPowerOfTwo(6), false);
+  EXPECT_EQ(IsPowerOfTwo(7), false);
+  EXPECT_EQ(IsPowerOfTwo(8), true);
+  EXPECT_EQ(IsPowerOfTwo(9), false);
+}
+
+TEST(MathTests, MaxAlignOf) {
+  EXPECT_EQ(MaxAlignOf(0u), 1u);
+  EXPECT_EQ(MaxAlignOf(1u), 1u);
+  EXPECT_EQ(MaxAlignOf(2u), 2u);
+  EXPECT_EQ(MaxAlignOf(3u), 1u);
+  EXPECT_EQ(MaxAlignOf(4u), 4u);
+  EXPECT_EQ(MaxAlignOf(5u), 1u);
+  EXPECT_EQ(MaxAlignOf(6u), 2u);
+  EXPECT_EQ(MaxAlignOf(7u), 1u);
+  EXPECT_EQ(MaxAlignOf(8u), 8u);
+  EXPECT_EQ(MaxAlignOf(9u), 1u);
+  EXPECT_EQ(MaxAlignOf(10u), 2u);
+  EXPECT_EQ(MaxAlignOf(11u), 1u);
+  EXPECT_EQ(MaxAlignOf(12u), 4u);
+  EXPECT_EQ(MaxAlignOf(13u), 1u);
+  EXPECT_EQ(MaxAlignOf(14u), 2u);
+  EXPECT_EQ(MaxAlignOf(15u), 1u);
+  EXPECT_EQ(MaxAlignOf(16u), 16u);
+}
+
+}  // namespace
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/reverse.h b/src/tint/utils/reverse.h
new file mode 100644
index 0000000..17b1346
--- /dev/null
+++ b/src/tint/utils/reverse.h
@@ -0,0 +1,64 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_REVERSE_H_
+#define SRC_TINT_UTILS_REVERSE_H_
+
+#include <iterator>
+
+namespace tint {
+namespace utils {
+
+namespace detail {
+/// Used by utils::Reverse to hold the underlying iterable.
+/// begin(ReverseIterable<T>) and end(ReverseIterable<T>) are automatically
+/// called for range-for loops, via argument-dependent lookup.
+/// See https://en.cppreference.com/w/cpp/language/range-for
+template <typename T>
+struct ReverseIterable {
+  /// The wrapped iterable object.
+  T& iterable;
+};
+
+template <typename T>
+auto begin(ReverseIterable<T> r_it) {
+  return std::rbegin(r_it.iterable);
+}
+
+template <typename T>
+auto end(ReverseIterable<T> r_it) {
+  return std::rend(r_it.iterable);
+}
+}  // namespace detail
+
+/// Reverse returns an iterable wrapper that when used in range-for loops,
+/// performs a reverse iteration over the object `iterable`.
+/// Example:
+/// ```
+/// /* Equivalent to:
+///  * for (auto it = vec.rbegin(); i != vec.rend(); ++it) {
+///  *   auto v = *it;
+///  */
+/// for (auto v : utils::Reverse(vec)) {
+/// }
+/// ```
+template <typename T>
+detail::ReverseIterable<T> Reverse(T&& iterable) {
+  return {iterable};
+}
+
+}  // namespace utils
+}  // namespace tint
+
+#endif  // SRC_TINT_UTILS_REVERSE_H_
diff --git a/src/tint/utils/reverse_test.cc b/src/tint/utils/reverse_test.cc
new file mode 100644
index 0000000..64734cd
--- /dev/null
+++ b/src/tint/utils/reverse_test.cc
@@ -0,0 +1,36 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/reverse.h"
+
+#include <vector>
+
+#include "gmock/gmock.h"
+
+namespace tint {
+namespace utils {
+namespace {
+
+TEST(ReverseTest, Vector) {
+  std::vector<int> vec{1, 3, 5, 7, 9};
+  std::vector<int> rev;
+  for (auto v : Reverse(vec)) {
+    rev.emplace_back(v);
+  }
+  ASSERT_THAT(rev, testing::ElementsAre(9, 7, 5, 3, 1));
+}
+
+}  // namespace
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/scoped_assignment.h b/src/tint/utils/scoped_assignment.h
new file mode 100644
index 0000000..0ff3421
--- /dev/null
+++ b/src/tint/utils/scoped_assignment.h
@@ -0,0 +1,64 @@
+
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_SCOPED_ASSIGNMENT_H_
+#define SRC_TINT_UTILS_SCOPED_ASSIGNMENT_H_
+
+#include <type_traits>
+
+#include "src/tint/utils/concat.h"
+
+namespace tint {
+namespace utils {
+
+/// Helper class that temporarily assigns a value to a variable for the lifetime
+/// of the ScopedAssignment object. Once the ScopedAssignment is destructed, the
+/// original value is restored.
+template <typename T>
+class ScopedAssignment {
+ public:
+  /// Constructor
+  /// @param var the variable to temporarily assign a new value to
+  /// @param val the value to assign to `ref` for the lifetime of this
+  /// ScopedAssignment.
+  ScopedAssignment(T& var, T val) : ref_(var) {
+    old_value_ = var;
+    var = val;
+  }
+
+  /// Destructor
+  /// Restores the original value of the variable.
+  ~ScopedAssignment() { ref_ = old_value_; }
+
+ private:
+  ScopedAssignment(const ScopedAssignment&) = delete;
+  ScopedAssignment& operator=(const ScopedAssignment&) = delete;
+
+  T& ref_;
+  T old_value_;
+};
+
+}  // namespace utils
+}  // namespace tint
+
+/// TINT_SCOPED_ASSIGNMENT(var, val) assigns `val` to `var`, and automatically
+/// restores the original value of `var` when exiting the current lexical scope.
+#define TINT_SCOPED_ASSIGNMENT(var, val)                                  \
+  ::tint::utils::ScopedAssignment<std::remove_reference_t<decltype(var)>> \
+  TINT_CONCAT(tint_scoped_assignment_, __COUNTER__) {                     \
+    var, val                                                              \
+  }
+
+#endif  // SRC_TINT_UTILS_SCOPED_ASSIGNMENT_H_
diff --git a/src/tint/utils/scoped_assignment_test.cc b/src/tint/utils/scoped_assignment_test.cc
new file mode 100644
index 0000000..20cd8d0
--- /dev/null
+++ b/src/tint/utils/scoped_assignment_test.cc
@@ -0,0 +1,47 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/scoped_assignment.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace utils {
+namespace {
+
+TEST(ScopedAssignmentTest, Scopes) {
+  int i = 0;
+  EXPECT_EQ(i, 0);
+  {
+    EXPECT_EQ(i, 0);
+    TINT_SCOPED_ASSIGNMENT(i, 1);
+    EXPECT_EQ(i, 1);
+    {
+      EXPECT_EQ(i, 1);
+      TINT_SCOPED_ASSIGNMENT(i, 2);
+      EXPECT_EQ(i, 2);
+    }
+    {
+      EXPECT_EQ(i, 1);
+      TINT_SCOPED_ASSIGNMENT(i, 3);
+      EXPECT_EQ(i, 3);
+    }
+    EXPECT_EQ(i, 1);
+  }
+  EXPECT_EQ(i, 0);
+}
+
+}  // namespace
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/string.h b/src/tint/utils/string.h
new file mode 100644
index 0000000..0355819
--- /dev/null
+++ b/src/tint/utils/string.h
@@ -0,0 +1,41 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_STRING_H_
+#define SRC_TINT_UTILS_STRING_H_
+
+#include <string>
+
+namespace tint {
+namespace utils {
+
+/// @param str the string to apply replacements to
+/// @param substr the string to search for
+/// @param replacement the replacement string to use instead of `substr`
+/// @returns `str` with all occurrences of `substr` replaced with `replacement`
+inline std::string ReplaceAll(std::string str,
+                              const std::string& substr,
+                              const std::string& replacement) {
+  size_t pos = 0;
+  while ((pos = str.find(substr, pos)) != std::string::npos) {
+    str.replace(pos, substr.length(), replacement);
+    pos += replacement.length();
+  }
+  return str;
+}
+
+}  // namespace utils
+}  // namespace tint
+
+#endif  // SRC_TINT_UTILS_STRING_H_
diff --git a/src/tint/utils/string_test.cc b/src/tint/utils/string_test.cc
new file mode 100644
index 0000000..3dbb486
--- /dev/null
+++ b/src/tint/utils/string_test.cc
@@ -0,0 +1,38 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/string.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace utils {
+namespace {
+
+TEST(StringTest, ReplaceAll) {
+  ASSERT_EQ("xybbcc", ReplaceAll("aabbcc", "aa", "xy"));
+  ASSERT_EQ("aaxycc", ReplaceAll("aabbcc", "bb", "xy"));
+  ASSERT_EQ("aabbxy", ReplaceAll("aabbcc", "cc", "xy"));
+  ASSERT_EQ("xyxybbcc", ReplaceAll("aabbcc", "a", "xy"));
+  ASSERT_EQ("aaxyxycc", ReplaceAll("aabbcc", "b", "xy"));
+  ASSERT_EQ("aabbxyxy", ReplaceAll("aabbcc", "c", "xy"));
+  // Replacement string includes the searched-for string.
+  // This proves that the algorithm needs to advance 'pos'
+  // past the replacement.
+  ASSERT_EQ("aabxybbxybcc", ReplaceAll("aabbcc", "b", "bxyb"));
+}
+
+}  // namespace
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/to_const_ptr_vec.h b/src/tint/utils/to_const_ptr_vec.h
new file mode 100644
index 0000000..e36fe0c
--- /dev/null
+++ b/src/tint/utils/to_const_ptr_vec.h
@@ -0,0 +1,39 @@
+
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_TO_CONST_PTR_VEC_H_
+#define SRC_TINT_UTILS_TO_CONST_PTR_VEC_H_
+
+#include <vector>
+
+namespace tint {
+namespace utils {
+
+/// @param in a vector of `T*`
+/// @returns a vector of `const T*` with the content of `in`.
+template <typename T>
+std::vector<const T*> ToConstPtrVec(const std::vector<T*>& in) {
+  std::vector<const T*> out;
+  out.reserve(in.size());
+  for (auto* ptr : in) {
+    out.emplace_back(ptr);
+  }
+  return out;
+}
+
+}  // namespace utils
+}  // namespace tint
+
+#endif  // SRC_TINT_UTILS_TO_CONST_PTR_VEC_H_
diff --git a/src/tint/utils/transform.h b/src/tint/utils/transform.h
new file mode 100644
index 0000000..97711fd
--- /dev/null
+++ b/src/tint/utils/transform.h
@@ -0,0 +1,62 @@
+// Copyright 2021 The Tint Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_TRANSFORM_H_
+#define SRC_TINT_UTILS_TRANSFORM_H_
+
+#include <algorithm>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+#include "src/tint/traits.h"
+
+namespace tint {
+namespace utils {
+
+/// Transform performs an element-wise transformation of a vector.
+/// @param in the input vector.
+/// @param transform the transformation function with signature: `OUT(IN)`
+/// @returns a new vector with each element of the source vector transformed by
+/// `transform`.
+template <typename IN, typename TRANSFORMER>
+auto Transform(const std::vector<IN>& in, TRANSFORMER&& transform)
+    -> std::vector<decltype(transform(in[0]))> {
+  std::vector<decltype(transform(in[0]))> result(in.size());
+  for (size_t i = 0; i < result.size(); ++i) {
+    result[i] = transform(in[i]);
+  }
+  return result;
+}
+
+/// Transform performs an element-wise transformation of a vector.
+/// @param in the input vector.
+/// @param transform the transformation function with signature:
+/// `OUT(IN, size_t)`
+/// @returns a new vector with each element of the source vector transformed by
+/// `transform`.
+template <typename IN, typename TRANSFORMER>
+auto Transform(const std::vector<IN>& in, TRANSFORMER&& transform)
+    -> std::vector<decltype(transform(in[0], 1u))> {
+  std::vector<decltype(transform(in[0], 1u))> result(in.size());
+  for (size_t i = 0; i < result.size(); ++i) {
+    result[i] = transform(in[i], i);
+  }
+  return result;
+}
+
+}  // namespace utils
+}  // namespace tint
+
+#endif  // SRC_TINT_UTILS_TRANSFORM_H_
diff --git a/src/tint/utils/transform_test.cc b/src/tint/utils/transform_test.cc
new file mode 100644
index 0000000..6686eeb
--- /dev/null
+++ b/src/tint/utils/transform_test.cc
@@ -0,0 +1,94 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/transform.h"
+
+#include <string>
+#include <type_traits>
+
+#include "gmock/gmock.h"
+
+#define CHECK_ELEMENT_TYPE(vector, expected)                                 \
+  static_assert(std::is_same<decltype(vector)::value_type, expected>::value, \
+                "unexpected result vector element type")
+
+namespace tint {
+namespace utils {
+namespace {
+
+TEST(TransformTest, Empty) {
+  const std::vector<int> empty{};
+  {
+    auto transformed = Transform(empty, [](int) -> int {
+      [] { FAIL() << "Transform should not be called for empty vector"; }();
+      return 0;
+    });
+    CHECK_ELEMENT_TYPE(transformed, int);
+    EXPECT_EQ(transformed.size(), 0u);
+  }
+  {
+    auto transformed = Transform(empty, [](int, size_t) -> int {
+      [] { FAIL() << "Transform should not be called for empty vector"; }();
+      return 0;
+    });
+    CHECK_ELEMENT_TYPE(transformed, int);
+    EXPECT_EQ(transformed.size(), 0u);
+  }
+}
+
+TEST(TransformTest, Identity) {
+  const std::vector<int> input{1, 2, 3, 4};
+  {
+    auto transformed = Transform(input, [](int i) { return i; });
+    CHECK_ELEMENT_TYPE(transformed, int);
+    EXPECT_THAT(transformed, testing::ElementsAre(1, 2, 3, 4));
+  }
+  {
+    auto transformed = Transform(input, [](int i, size_t) { return i; });
+    CHECK_ELEMENT_TYPE(transformed, int);
+    EXPECT_THAT(transformed, testing::ElementsAre(1, 2, 3, 4));
+  }
+}
+
+TEST(TransformTest, Index) {
+  const std::vector<int> input{10, 20, 30, 40};
+  {
+    auto transformed = Transform(input, [](int, size_t idx) { return idx; });
+    CHECK_ELEMENT_TYPE(transformed, size_t);
+    EXPECT_THAT(transformed, testing::ElementsAre(0u, 1u, 2u, 3u));
+  }
+}
+
+TEST(TransformTest, TransformSameType) {
+  const std::vector<int> input{1, 2, 3, 4};
+  {
+    auto transformed = Transform(input, [](int i) { return i * 10; });
+    CHECK_ELEMENT_TYPE(transformed, int);
+    EXPECT_THAT(transformed, testing::ElementsAre(10, 20, 30, 40));
+  }
+}
+
+TEST(TransformTest, TransformDifferentType) {
+  const std::vector<int> input{1, 2, 3, 4};
+  {
+    auto transformed =
+        Transform(input, [](int i) { return std::to_string(i); });
+    CHECK_ELEMENT_TYPE(transformed, std::string);
+    EXPECT_THAT(transformed, testing::ElementsAre("1", "2", "3", "4"));
+  }
+}
+
+}  // namespace
+}  // namespace utils
+}  // namespace tint
diff --git a/src/tint/utils/unique_vector.h b/src/tint/utils/unique_vector.h
new file mode 100644
index 0000000..96d3ac3
--- /dev/null
+++ b/src/tint/utils/unique_vector.h
@@ -0,0 +1,113 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_UNIQUE_VECTOR_H_
+#define SRC_TINT_UTILS_UNIQUE_VECTOR_H_
+
+#include <cstddef>
+#include <functional>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+namespace tint {
+namespace utils {
+
+/// UniqueVector is an ordered container that only contains unique items.
+/// Attempting to add a duplicate is a no-op.
+template <typename T,
+          typename HASH = std::hash<T>,
+          typename EQUAL = std::equal_to<T>>
+struct UniqueVector {
+  /// The iterator returned by begin() and end()
+  using ConstIterator = typename std::vector<T>::const_iterator;
+  /// The iterator returned by rbegin() and rend()
+  using ConstReverseIterator = typename std::vector<T>::const_reverse_iterator;
+
+  /// Constructor
+  UniqueVector() = default;
+
+  /// Constructor
+  /// @param v the vector to construct this UniqueVector with. Duplicate
+  /// elements will be removed.
+  explicit UniqueVector(std::vector<T>&& v) {
+    for (auto& el : v) {
+      add(el);
+    }
+  }
+
+  /// add appends the item to the end of the vector, if the vector does not
+  /// already contain the given item.
+  /// @param item the item to append to the end of the vector
+  /// @returns true if the item was added, otherwise false.
+  bool add(const T& item) {
+    if (set.count(item) == 0) {
+      vector.emplace_back(item);
+      set.emplace(item);
+      return true;
+    }
+    return false;
+  }
+
+  /// @returns true if the vector contains `item`
+  /// @param item the item
+  bool contains(const T& item) const { return set.count(item); }
+
+  /// @param i the index of the element to retrieve
+  /// @returns the element at the index `i`
+  T& operator[](size_t i) { return vector[i]; }
+
+  /// @param i the index of the element to retrieve
+  /// @returns the element at the index `i`
+  const T& operator[](size_t i) const { return vector[i]; }
+
+  /// @returns true if the vector is empty
+  bool empty() const { return vector.empty(); }
+
+  /// @returns the number of items in the vector
+  size_t size() const { return vector.size(); }
+
+  /// @returns an iterator to the beginning of the vector
+  ConstIterator begin() const { return vector.begin(); }
+
+  /// @returns an iterator to the end of the vector
+  ConstIterator end() const { return vector.end(); }
+
+  /// @returns an iterator to the beginning of the reversed vector
+  ConstReverseIterator rbegin() const { return vector.rbegin(); }
+
+  /// @returns an iterator to the end of the reversed vector
+  ConstReverseIterator rend() const { return vector.rend(); }
+
+  /// @returns a const reference to the internal vector
+  operator const std::vector<T>&() const { return vector; }
+
+  /// Removes the last element from the vector
+  /// @returns the popped element
+  T pop_back() {
+    auto el = std::move(vector.back());
+    set.erase(el);
+    vector.pop_back();
+    return el;
+  }
+
+ private:
+  std::vector<T> vector;
+  std::unordered_set<T, HASH, EQUAL> set;
+};
+
+}  // namespace utils
+}  // namespace tint
+
+#endif  // SRC_TINT_UTILS_UNIQUE_VECTOR_H_
diff --git a/src/tint/utils/unique_vector_test.cc b/src/tint/utils/unique_vector_test.cc
new file mode 100644
index 0000000..892282b
--- /dev/null
+++ b/src/tint/utils/unique_vector_test.cc
@@ -0,0 +1,145 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/unique_vector.h"
+#include "src/tint/utils/reverse.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace utils {
+namespace {
+
+TEST(UniqueVectorTest, Empty) {
+  UniqueVector<int> unique_vec;
+  EXPECT_EQ(unique_vec.size(), 0u);
+  EXPECT_EQ(unique_vec.empty(), true);
+  EXPECT_EQ(unique_vec.begin(), unique_vec.end());
+}
+
+TEST(UniqueVectorTest, MoveConstructor) {
+  UniqueVector<int> unique_vec(std::vector<int>{0, 3, 2, 1, 2});
+  EXPECT_EQ(unique_vec.size(), 4u);
+  EXPECT_EQ(unique_vec.empty(), false);
+  EXPECT_EQ(unique_vec[0], 0);
+  EXPECT_EQ(unique_vec[1], 3);
+  EXPECT_EQ(unique_vec[2], 2);
+  EXPECT_EQ(unique_vec[3], 1);
+}
+
+TEST(UniqueVectorTest, AddUnique) {
+  UniqueVector<int> unique_vec;
+  unique_vec.add(0);
+  unique_vec.add(1);
+  unique_vec.add(2);
+  EXPECT_EQ(unique_vec.size(), 3u);
+  EXPECT_EQ(unique_vec.empty(), false);
+  int i = 0;
+  for (auto n : unique_vec) {
+    EXPECT_EQ(n, i);
+    i++;
+  }
+  for (auto n : Reverse(unique_vec)) {
+    i--;
+    EXPECT_EQ(n, i);
+  }
+  EXPECT_EQ(unique_vec[0], 0);
+  EXPECT_EQ(unique_vec[1], 1);
+  EXPECT_EQ(unique_vec[2], 2);
+}
+
+TEST(UniqueVectorTest, AddDuplicates) {
+  UniqueVector<int> unique_vec;
+  unique_vec.add(0);
+  unique_vec.add(0);
+  unique_vec.add(0);
+  unique_vec.add(1);
+  unique_vec.add(1);
+  unique_vec.add(2);
+  EXPECT_EQ(unique_vec.size(), 3u);
+  EXPECT_EQ(unique_vec.empty(), false);
+  int i = 0;
+  for (auto n : unique_vec) {
+    EXPECT_EQ(n, i);
+    i++;
+  }
+  for (auto n : Reverse(unique_vec)) {
+    i--;
+    EXPECT_EQ(n, i);
+  }
+  EXPECT_EQ(unique_vec[0], 0);
+  EXPECT_EQ(unique_vec[1], 1);
+  EXPECT_EQ(unique_vec[2], 2);
+}
+
+TEST(UniqueVectorTest, AsVector) {
+  UniqueVector<int> unique_vec;
+  unique_vec.add(0);
+  unique_vec.add(0);
+  unique_vec.add(0);
+  unique_vec.add(1);
+  unique_vec.add(1);
+  unique_vec.add(2);
+
+  const std::vector<int>& vec = unique_vec;
+  EXPECT_EQ(vec.size(), 3u);
+  EXPECT_EQ(unique_vec.empty(), false);
+  int i = 0;
+  for (auto n : vec) {
+    EXPECT_EQ(n, i);
+    i++;
+  }
+  for (auto n : Reverse(unique_vec)) {
+    i--;
+    EXPECT_EQ(n, i);
+  }
+}
+
+TEST(UniqueVectorTest, PopBack) {
+  UniqueVector<int> unique_vec;
+  unique_vec.add(0);
+  unique_vec.add(2);
+  unique_vec.add(1);
+
+  EXPECT_EQ(unique_vec.pop_back(), 1);
+  EXPECT_EQ(unique_vec.size(), 2u);
+  EXPECT_EQ(unique_vec.empty(), false);
+  EXPECT_EQ(unique_vec[0], 0);
+  EXPECT_EQ(unique_vec[1], 2);
+
+  EXPECT_EQ(unique_vec.pop_back(), 2);
+  EXPECT_EQ(unique_vec.size(), 1u);
+  EXPECT_EQ(unique_vec.empty(), false);
+  EXPECT_EQ(unique_vec[0], 0);
+
+  unique_vec.add(1);
+
+  EXPECT_EQ(unique_vec.size(), 2u);
+  EXPECT_EQ(unique_vec.empty(), false);
+  EXPECT_EQ(unique_vec[0], 0);
+  EXPECT_EQ(unique_vec[1], 1);
+
+  EXPECT_EQ(unique_vec.pop_back(), 1);
+  EXPECT_EQ(unique_vec.size(), 1u);
+  EXPECT_EQ(unique_vec.empty(), false);
+  EXPECT_EQ(unique_vec[0], 0);
+
+  EXPECT_EQ(unique_vec.pop_back(), 0);
+  EXPECT_EQ(unique_vec.size(), 0u);
+  EXPECT_EQ(unique_vec.empty(), true);
+}
+
+}  // namespace
+}  // namespace utils
+}  // namespace tint