Import Tint changes from Dawn

Changes:
  - aa97bb5327c375e89ec1f0ee612b0e00093b9c34 [ir] Add assignment statements by dan sinclair <dsinclair@chromium.org>
  - 5b541ff3c287f06b74350827628249152293f407 [ir] Mark const variable as skipped. by dan sinclair <dsinclair@chromium.org>
  - 5aa7ef2bc077134fca8afbe584016916adecc064 [ir] Add Unary expressions by dan sinclair <dsinclair@chromium.org>
  - 339719967ebfb3493cb351bc6a8a82388bca1fd3 [ir] Remove internal `bool` returns. by dan sinclair <dsinclair@chromium.org>
  - 69108d048b56073ccc0ac7d46c254c62a87ac205 [ir] Add ir::Discard by dan sinclair <dsinclair@chromium.org>
  - ae39e6d62808b1b3899430c38f41f618ed9337d7 spirv-reader: Declare multiuse constant composites by James Price <jrprice@google.com>
  - a41693babb6069d4487a1cd2ab114aa87ee0fe78 tint/msl: Avoid cloning a built-in struct by James Price <jrprice@google.com>
  - 99a0ded622559bd21f063415299a2ee7b7514233 tint: Use type::Struct::Name in CreateASTTypeFor by James Price <jrprice@google.com>
  - 03f9f5f538121da7bc3c6175de9fd38fa2aaf0e0 [ir] Rename ir::Temp by dan sinclair <dsinclair@chromium.org>
  - d36740509fa6daa1200ee47f6dbbed66df652aeb spirv-reader: Error on multiple Position built-ins by James Price <jrprice@google.com>
  - bdc2d249007af5b71584ae45f61ca568d9bbfb32 Fix MSL invariant translation. by dan sinclair <dsinclair@chromium.org>
  - 378a1f51a289147d5fa8065c4f2ef25a757ef7ae Use optional in Lexer. by dan sinclair <dsinclair@chromium.org>
  - 889edb18b2740faa5efb35f5020103f7db2fb216 Add simple program for perf testing. by dan sinclair <dsinclair@chromium.org>
  - d14a9fbb6e3d398cbca9d7404eb6ffaa0fbe5646 tint/resolver: Fix null deref by Ben Clayton <bclayton@google.com>
GitOrigin-RevId: aa97bb5327c375e89ec1f0ee612b0e00093b9c34
Change-Id: I8fb8032fcea440d70d07e0fcd94bb4c05973e2ad
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/129040
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 7bac6ed..f715428 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -720,6 +720,8 @@
     ir/debug.h
     ir/disassembler.cc
     ir/disassembler.h
+    ir/discard.cc
+    ir/discard.h
     ir/flow_node.cc
     ir/flow_node.h
     ir/function.cc
@@ -732,12 +734,16 @@
     ir/loop.h
     ir/module.cc
     ir/module.h
+    ir/runtime.cc
+    ir/runtime.h
+    ir/store.cc
+    ir/store.h
     ir/switch.cc
     ir/switch.h
-    ir/temp.cc
-    ir/temp.h
     ir/terminator.cc
     ir/terminator.h
+    ir/unary.cc
+    ir/unary.h
     ir/user_call.cc
     ir/user_call.h
     ir/value.cc
@@ -1413,8 +1419,11 @@
       ir/bitcast_test.cc
       ir/builder_impl_test.cc
       ir/constant_test.cc
-      ir/temp_test.cc
+      ir/discard_test.cc
+      ir/runtime_test.cc
+      ir/store_test.cc
       ir/test_helper.h
+      ir/unary_test.cc
     )
   endif()
 
diff --git a/src/tint/cmd/CMakeLists.txt b/src/tint/cmd/CMakeLists.txt
index 142595b..441954d 100644
--- a/src/tint/cmd/CMakeLists.txt
+++ b/src/tint/cmd/CMakeLists.txt
@@ -53,3 +53,15 @@
 if (${TINT_BUILD_SPV_READER})
     target_link_libraries(tint_info SPIRV-Tools)
 endif()
+
+add_executable(tint-loopy "")
+target_sources(tint-loopy PRIVATE
+  "generate_external_texture_bindings.cc"
+  "generate_external_texture_bindings.h"
+  "helper.cc"
+  "helper.h"
+  "loopy.cc"
+)
+tint_default_compile_options(tint-loopy)
+target_link_libraries(tint-loopy libtint tint_val)
+
diff --git a/src/tint/cmd/helper.cc b/src/tint/cmd/helper.cc
index 6ff1e7a..a00e24a 100644
--- a/src/tint/cmd/helper.cc
+++ b/src/tint/cmd/helper.cc
@@ -45,54 +45,6 @@
     return input_format;
 }
 
-/// Copies the content from the file named `input_file` to `buffer`,
-/// assuming each element in the file is of type `T`.  If any error occurs,
-/// writes error messages to the standard error stream and returns false.
-/// Assumes the size of a `T` object is divisible by its required alignment.
-/// @returns true if we successfully read the file.
-template <typename T>
-bool ReadFile(const std::string& input_file, std::vector<T>* buffer) {
-    if (!buffer) {
-        std::cerr << "The buffer pointer was null" << std::endl;
-        return false;
-    }
-
-    FILE* file = nullptr;
-#if defined(_MSC_VER)
-    fopen_s(&file, input_file.c_str(), "rb");
-#else
-    file = fopen(input_file.c_str(), "rb");
-#endif
-    if (!file) {
-        std::cerr << "Failed to open " << input_file << std::endl;
-        return false;
-    }
-
-    fseek(file, 0, SEEK_END);
-    const auto file_size = static_cast<size_t>(ftell(file));
-    if (0 != (file_size % sizeof(T))) {
-        std::cerr << "File " << input_file
-                  << " does not contain an integral number of objects: " << file_size
-                  << " bytes in the file, require " << sizeof(T) << " bytes per object"
-                  << std::endl;
-        fclose(file);
-        return false;
-    }
-    fseek(file, 0, SEEK_SET);
-
-    buffer->clear();
-    buffer->resize(file_size / sizeof(T));
-
-    size_t bytes_read = fread(buffer->data(), 1, file_size, file);
-    fclose(file);
-    if (bytes_read != file_size) {
-        std::cerr << "Failed to read " << input_file << std::endl;
-        return false;
-    }
-
-    return true;
-}
-
 void PrintBindings(tint::inspector::Inspector& inspector, const std::string& ep_name) {
     auto bindings = inspector.GetResourceBindings(ep_name);
     if (!inspector.error().empty()) {
diff --git a/src/tint/cmd/helper.h b/src/tint/cmd/helper.h
index 3a48ea4..6878288 100644
--- a/src/tint/cmd/helper.h
+++ b/src/tint/cmd/helper.h
@@ -15,8 +15,10 @@
 #ifndef SRC_TINT_CMD_HELPER_H_
 #define SRC_TINT_CMD_HELPER_H_
 
+#include <iostream>
 #include <memory>
 #include <string>
+#include <vector>
 
 #include "tint/tint.h"
 
@@ -102,6 +104,54 @@
 /// @return the text name
 std::string OverrideTypeToString(tint::inspector::Override::Type type);
 
+/// Copies the content from the file named `input_file` to `buffer`,
+/// assuming each element in the file is of type `T`.  If any error occurs,
+/// writes error messages to the standard error stream and returns false.
+/// Assumes the size of a `T` object is divisible by its required alignment.
+/// @returns true if we successfully read the file.
+template <typename T>
+bool ReadFile(const std::string& input_file, std::vector<T>* buffer) {
+    if (!buffer) {
+        std::cerr << "The buffer pointer was null" << std::endl;
+        return false;
+    }
+
+    FILE* file = nullptr;
+#if defined(_MSC_VER)
+    fopen_s(&file, input_file.c_str(), "rb");
+#else
+    file = fopen(input_file.c_str(), "rb");
+#endif
+    if (!file) {
+        std::cerr << "Failed to open " << input_file << std::endl;
+        return false;
+    }
+
+    fseek(file, 0, SEEK_END);
+    const auto file_size = static_cast<size_t>(ftell(file));
+    if (0 != (file_size % sizeof(T))) {
+        std::cerr << "File " << input_file
+                  << " does not contain an integral number of objects: " << file_size
+                  << " bytes in the file, require " << sizeof(T) << " bytes per object"
+                  << std::endl;
+        fclose(file);
+        return false;
+    }
+    fseek(file, 0, SEEK_SET);
+
+    buffer->clear();
+    buffer->resize(file_size / sizeof(T));
+
+    size_t bytes_read = fread(buffer->data(), 1, file_size, file);
+    fclose(file);
+    if (bytes_read != file_size) {
+        std::cerr << "Failed to read " << input_file << std::endl;
+        return false;
+    }
+
+    return true;
+}
+
 }  // namespace tint::cmd
 
 #endif  // SRC_TINT_CMD_HELPER_H_
diff --git a/src/tint/cmd/loopy.cc b/src/tint/cmd/loopy.cc
new file mode 100644
index 0000000..1714e3b
--- /dev/null
+++ b/src/tint/cmd/loopy.cc
@@ -0,0 +1,433 @@
+// Copyright 2023 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 <iostream>
+
+#include "src/tint/cmd/generate_external_texture_bindings.h"
+#include "src/tint/cmd/helper.h"
+#include "tint/tint.h"
+
+#if TINT_BUILD_IR
+#include "src/tint/ir/module.h"
+#endif  // TINT_BUILD_IR
+
+namespace {
+
+enum class Format {
+    kUnknown,
+    kNone,
+    kSpirv,
+    kWgsl,
+    kMsl,
+    kHlsl,
+    kGlsl,
+};
+
+enum class Looper {
+    kLoad,
+    kIRGenerate,
+    kWriter,
+};
+
+struct Options {
+    bool show_help = false;
+
+    std::string input_filename;
+    Format format = Format::kUnknown;
+
+    Looper loop = Looper::kLoad;
+    uint32_t loop_count = 100;
+};
+
+const char kUsage[] = R"(Usage: tint-loopy [options] <input-file>
+
+ options:
+  --format <spirv|wgsl|msl|hlsl|none>  -- Generation format. Default SPIR-V.
+  --loop <load,ir-gen,writer>          -- Item to loop
+  --loop-count <num>                   -- Number of loops to run, default 100.
+)";
+
+Format parse_format(const std::string& fmt) {
+    (void)fmt;
+
+#if TINT_BUILD_SPV_WRITER
+    if (fmt == "spirv") {
+        return Format::kSpirv;
+    }
+#endif  // TINT_BUILD_SPV_WRITER
+
+#if TINT_BUILD_WGSL_WRITER
+    if (fmt == "wgsl") {
+        return Format::kWgsl;
+    }
+#endif  // TINT_BUILD_WGSL_WRITER
+
+#if TINT_BUILD_MSL_WRITER
+    if (fmt == "msl") {
+        return Format::kMsl;
+    }
+#endif  // TINT_BUILD_MSL_WRITER
+
+#if TINT_BUILD_HLSL_WRITER
+    if (fmt == "hlsl") {
+        return Format::kHlsl;
+    }
+#endif  // TINT_BUILD_HLSL_WRITER
+
+#if TINT_BUILD_GLSL_WRITER
+    if (fmt == "glsl") {
+        return Format::kGlsl;
+    }
+#endif  // TINT_BUILD_GLSL_WRITER
+
+    if (fmt == "none") {
+        return Format::kNone;
+    }
+
+    return Format::kUnknown;
+}
+
+bool ParseArgs(const std::vector<std::string>& args, Options* opts) {
+    for (size_t i = 1; i < args.size(); ++i) {
+        const std::string& arg = args[i];
+        if (arg == "--format") {
+            ++i;
+            if (i >= args.size()) {
+                std::cerr << "Missing value for --format argument." << std::endl;
+                return false;
+            }
+            opts->format = parse_format(args[i]);
+
+            if (opts->format == Format::kUnknown) {
+                std::cerr << "Unknown output format: " << args[i] << std::endl;
+                return false;
+            }
+        } else if (arg == "-h" || arg == "--help") {
+            opts->show_help = true;
+        } else if (arg == "--loop") {
+            ++i;
+            if (i >= args.size()) {
+                std::cerr << "Missing value for --loop argument." << std::endl;
+                return false;
+            }
+            if (args[i] == "load") {
+                opts->loop = Looper::kLoad;
+            } else if (args[i] == "ir-gen") {
+                opts->loop = Looper::kIRGenerate;
+            } else if (args[i] == "writer") {
+                opts->loop = Looper::kWriter;
+            } else {
+                std::cerr << "Invalid loop value" << std::endl;
+                return false;
+            }
+        } else if (arg == "--loop-count") {
+            ++i;
+            if (i >= args.size()) {
+                std::cerr << "Missing value for --loop-count argument." << std::endl;
+                return false;
+            }
+            int32_t val = atoi(args[i].c_str());
+            if (val <= 0) {
+                std::cerr << "Loop count must be greater then 0" << std::endl;
+                return false;
+            }
+            opts->loop_count = static_cast<uint32_t>(val);
+        } else if (!arg.empty()) {
+            if (arg[0] == '-') {
+                std::cerr << "Unrecognized option: " << arg << std::endl;
+                return false;
+            }
+            if (!opts->input_filename.empty()) {
+                std::cerr << "More than one input file specified: '" << opts->input_filename
+                          << "' and '" << arg << "'" << std::endl;
+                return false;
+            }
+            opts->input_filename = arg;
+        }
+    }
+    return true;
+}
+
+/// Generate SPIR-V code for a program.
+/// @param program the program to generate
+/// @returns true on success
+bool GenerateSpirv(const tint::Program* program) {
+#if TINT_BUILD_SPV_WRITER
+    tint::writer::spirv::Options gen_options;
+    gen_options.external_texture_options.bindings_map =
+        tint::cmd::GenerateExternalTextureBindings(program);
+    auto result = tint::writer::spirv::Generate(program, gen_options);
+    if (!result.success) {
+        tint::cmd::PrintWGSL(std::cerr, *program);
+        std::cerr << "Failed to generate: " << result.error << std::endl;
+        return false;
+    }
+    return true;
+#else
+    (void)program;
+    std::cerr << "SPIR-V writer not enabled in tint build" << std::endl;
+    return false;
+#endif  // TINT_BUILD_SPV_WRITER
+}
+
+/// Generate WGSL code for a program.
+/// @param program the program to generate
+/// @returns true on success
+bool GenerateWgsl(const tint::Program* program) {
+#if TINT_BUILD_WGSL_WRITER
+    tint::writer::wgsl::Options gen_options;
+    auto result = tint::writer::wgsl::Generate(program, gen_options);
+    if (!result.success) {
+        std::cerr << "Failed to generate: " << result.error << std::endl;
+        return false;
+    }
+
+    return true;
+#else
+    (void)program;
+    std::cerr << "WGSL writer not enabled in tint build" << std::endl;
+    return false;
+#endif  // TINT_BUILD_WGSL_WRITER
+}
+
+/// Generate MSL code for a program.
+/// @param program the program to generate
+/// @returns true on success
+bool GenerateMsl(const tint::Program* program) {
+#if TINT_BUILD_MSL_WRITER
+    // Remap resource numbers to a flat namespace.
+    // TODO(crbug.com/tint/1501): Do this via Options::BindingMap.
+    const tint::Program* input_program = program;
+    auto flattened = tint::writer::FlattenBindings(program);
+    if (flattened) {
+        input_program = &*flattened;
+    }
+
+    tint::writer::msl::Options gen_options;
+    gen_options.external_texture_options.bindings_map =
+        tint::cmd::GenerateExternalTextureBindings(input_program);
+    gen_options.array_length_from_uniform.ubo_binding = tint::writer::BindingPoint{0, 30};
+    gen_options.array_length_from_uniform.bindpoint_to_size_index.emplace(
+        tint::writer::BindingPoint{0, 0}, 0);
+    gen_options.array_length_from_uniform.bindpoint_to_size_index.emplace(
+        tint::writer::BindingPoint{0, 1}, 1);
+    auto result = tint::writer::msl::Generate(input_program, gen_options);
+    if (!result.success) {
+        tint::cmd::PrintWGSL(std::cerr, *program);
+        std::cerr << "Failed to generate: " << result.error << std::endl;
+        return false;
+    }
+
+    return true;
+#else
+    (void)program;
+    std::cerr << "MSL writer not enabled in tint build" << std::endl;
+    return false;
+#endif  // TINT_BUILD_MSL_WRITER
+}
+
+/// Generate HLSL code for a program.
+/// @param program the program to generate
+/// @returns true on success
+bool GenerateHlsl(const tint::Program* program) {
+#if TINT_BUILD_HLSL_WRITER
+    tint::writer::hlsl::Options gen_options;
+    gen_options.external_texture_options.bindings_map =
+        tint::cmd::GenerateExternalTextureBindings(program);
+    auto result = tint::writer::hlsl::Generate(program, gen_options);
+    if (!result.success) {
+        tint::cmd::PrintWGSL(std::cerr, *program);
+        std::cerr << "Failed to generate: " << result.error << std::endl;
+        return false;
+    }
+
+    return true;
+#else
+    (void)program;
+    std::cerr << "HLSL writer not enabled in tint build" << std::endl;
+    return false;
+#endif  // TINT_BUILD_HLSL_WRITER
+}
+
+/// Generate GLSL code for a program.
+/// @param program the program to generate
+/// @returns true on success
+bool GenerateGlsl(const tint::Program* program) {
+#if TINT_BUILD_GLSL_WRITER
+    tint::writer::glsl::Options gen_options;
+    gen_options.external_texture_options.bindings_map =
+        tint::cmd::GenerateExternalTextureBindings(program);
+    auto result = tint::writer::glsl::Generate(program, gen_options, "");
+    if (!result.success) {
+        tint::cmd::PrintWGSL(std::cerr, *program);
+        std::cerr << "Failed to generate: " << result.error << std::endl;
+        return false;
+    }
+
+    return true;
+
+#else
+    (void)program;
+    std::cerr << "GLSL writer not enabled in tint build" << std::endl;
+    return false;
+#endif  // TINT_BUILD_GLSL_WRITER
+}
+
+}  // namespace
+
+int main(int argc, const char** argv) {
+    std::vector<std::string> args(argv, argv + argc);
+    Options options;
+
+    tint::SetInternalCompilerErrorReporter(&tint::cmd::TintInternalCompilerErrorReporter);
+
+#if TINT_BUILD_WGSL_WRITER
+    tint::Program::printer = [](const tint::Program* program) {
+        auto result = tint::writer::wgsl::Generate(program, {});
+        if (!result.error.empty()) {
+            return "error: " + result.error;
+        }
+        return result.wgsl;
+    };
+#endif  // TINT_BUILD_WGSL_WRITER
+
+    if (!ParseArgs(args, &options)) {
+        std::cerr << "Failed to parse arguments." << std::endl;
+        return 1;
+    }
+
+    if (options.show_help) {
+        std::cout << kUsage << std::endl;
+        return 0;
+    }
+
+    // Implement output format defaults.
+    if (options.format == Format::kUnknown) {
+        options.format = Format::kSpirv;
+    }
+
+    auto diag_printer = tint::diag::Printer::create(stderr, true);
+    tint::diag::Formatter diag_formatter;
+
+    std::unique_ptr<tint::Program> program;
+    std::unique_ptr<tint::Source::File> source_file;
+
+    if (options.loop == Looper::kLoad) {
+        if (options.input_filename.size() > 5 &&
+            options.input_filename.substr(options.input_filename.size() - 5) == ".wgsl") {
+#if TINT_BUILD_WGSL_READER
+            std::vector<uint8_t> data;
+            if (!tint::cmd::ReadFile<uint8_t>(options.input_filename, &data)) {
+                exit(1);
+            }
+            source_file = std::make_unique<tint::Source::File>(
+                options.input_filename, std::string(data.begin(), data.end()));
+
+            uint32_t loop_count = options.loop_count;
+            for (uint32_t i = 0; i < loop_count; ++i) {
+                program =
+                    std::make_unique<tint::Program>(tint::reader::wgsl::Parse(source_file.get()));
+            }
+#else
+            std::cerr << "Tint not built with the WGSL reader enabled" << std::endl;
+            exit(1);
+#endif  // TINT_BUILD_WGSL_READER
+        } else {
+#if TINT_BUILD_SPV_READER
+            std::vector<uint32_t> data;
+            if (!tint::cmd::ReadFile<uint32_t>(options.input_filename, &data)) {
+                exit(1);
+            }
+
+            uint32_t loop_count = options.loop_count;
+            for (uint32_t i = 0; i < loop_count; ++i) {
+                program = std::make_unique<tint::Program>(tint::reader::spirv::Parse(data, {}));
+            }
+#else
+            std::cerr << "Tint not built with the SPIR-V reader enabled" << std::endl;
+            exit(1);
+#endif  // TINT_BUILD_SPV_READER
+        }
+    }
+
+    // Load the program that will actually be used
+    {
+        tint::cmd::LoadProgramOptions opts;
+        opts.filename = options.input_filename;
+
+        auto info = tint::cmd::LoadProgramInfo(opts);
+        program = std::move(info.program);
+        source_file = std::move(info.source_file);
+    }
+#if TINT_BUILD_IR
+    {
+        uint32_t loop_count = 1;
+        if (options.loop == Looper::kIRGenerate) {
+            loop_count = options.loop_count;
+        }
+        for (uint32_t i = 0; i < loop_count; ++i) {
+            auto result = tint::ir::Module::FromProgram(program.get());
+            if (!result) {
+                std::cerr << "Failed to build IR from program: " << result.Failure() << std::endl;
+            }
+        }
+    }
+#endif  // TINT_BUILD_IR
+
+    bool success = false;
+    {
+        uint32_t loop_count = 1;
+        if (options.loop == Looper::kWriter) {
+            loop_count = options.loop_count;
+        }
+
+        switch (options.format) {
+            case Format::kSpirv:
+                for (uint32_t i = 0; i < loop_count; ++i) {
+                    success = GenerateSpirv(program.get());
+                }
+                break;
+            case Format::kWgsl:
+                for (uint32_t i = 0; i < loop_count; ++i) {
+                    success = GenerateWgsl(program.get());
+                }
+                break;
+            case Format::kMsl:
+                for (uint32_t i = 0; i < loop_count; ++i) {
+                    success = GenerateMsl(program.get());
+                }
+                break;
+            case Format::kHlsl:
+                for (uint32_t i = 0; i < loop_count; ++i) {
+                    success = GenerateHlsl(program.get());
+                }
+                break;
+            case Format::kGlsl:
+                for (uint32_t i = 0; i < loop_count; ++i) {
+                    success = GenerateGlsl(program.get());
+                }
+                break;
+            case Format::kNone:
+                break;
+            default:
+                std::cerr << "Unknown output format specified" << std::endl;
+                return 1;
+        }
+    }
+    if (!success) {
+        return 1;
+    }
+
+    return 0;
+}
diff --git a/src/tint/ir/binary.h b/src/tint/ir/binary.h
index e39758f..400e381 100644
--- a/src/tint/ir/binary.h
+++ b/src/tint/ir/binary.h
@@ -16,8 +16,6 @@
 #define SRC_TINT_IR_BINARY_H_
 
 #include "src/tint/ir/instruction.h"
-#include "src/tint/symbol_table.h"
-#include "src/tint/type/type.h"
 #include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
diff --git a/src/tint/ir/binary_test.cc b/src/tint/ir/binary_test.cc
index 31972ce..6be82f9 100644
--- a/src/tint/ir/binary_test.cc
+++ b/src/tint/ir/binary_test.cc
@@ -26,15 +26,15 @@
 TEST_F(IR_InstructionTest, CreateAnd) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.And(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
                                       b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kAnd);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
     ASSERT_NE(instr->Result()->Type(), nullptr);
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -54,14 +54,14 @@
 TEST_F(IR_InstructionTest, CreateOr) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.Or(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
                                      b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kOr);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -81,14 +81,14 @@
 TEST_F(IR_InstructionTest, CreateXor) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.Xor(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
                                       b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kXor);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -108,14 +108,14 @@
 TEST_F(IR_InstructionTest, CreateLogicalAnd) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.LogicalAnd(b.builder.ir.types.Get<type::Bool>(),
                                              b.builder.Constant(4_i), b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kLogicalAnd);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -135,14 +135,14 @@
 TEST_F(IR_InstructionTest, CreateLogicalOr) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.LogicalOr(b.builder.ir.types.Get<type::Bool>(),
                                             b.builder.Constant(4_i), b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kLogicalOr);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -162,14 +162,14 @@
 TEST_F(IR_InstructionTest, CreateEqual) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.Equal(b.builder.ir.types.Get<type::Bool>(),
                                         b.builder.Constant(4_i), b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kEqual);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -189,14 +189,14 @@
 TEST_F(IR_InstructionTest, CreateNotEqual) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.NotEqual(b.builder.ir.types.Get<type::Bool>(),
                                            b.builder.Constant(4_i), b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kNotEqual);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -216,14 +216,14 @@
 TEST_F(IR_InstructionTest, CreateLessThan) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.LessThan(b.builder.ir.types.Get<type::Bool>(),
                                            b.builder.Constant(4_i), b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kLessThan);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -243,14 +243,14 @@
 TEST_F(IR_InstructionTest, CreateGreaterThan) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.GreaterThan(b.builder.ir.types.Get<type::Bool>(),
                                               b.builder.Constant(4_i), b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kGreaterThan);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -270,14 +270,14 @@
 TEST_F(IR_InstructionTest, CreateLessThanEqual) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.LessThanEqual(b.builder.ir.types.Get<type::Bool>(),
                                                 b.builder.Constant(4_i), b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kLessThanEqual);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -297,14 +297,14 @@
 TEST_F(IR_InstructionTest, CreateGreaterThanEqual) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.GreaterThanEqual(
         b.builder.ir.types.Get<type::Bool>(), b.builder.Constant(4_i), b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kGreaterThanEqual);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -324,14 +324,14 @@
 TEST_F(IR_InstructionTest, CreateShiftLeft) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.ShiftLeft(b.builder.ir.types.Get<type::I32>(),
                                             b.builder.Constant(4_i), b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kShiftLeft);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -351,14 +351,14 @@
 TEST_F(IR_InstructionTest, CreateShiftRight) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.ShiftRight(b.builder.ir.types.Get<type::I32>(),
                                              b.builder.Constant(4_i), b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kShiftRight);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -378,14 +378,14 @@
 TEST_F(IR_InstructionTest, CreateAdd) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.Add(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
                                       b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kAdd);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -405,14 +405,14 @@
 TEST_F(IR_InstructionTest, CreateSubtract) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.Subtract(b.builder.ir.types.Get<type::I32>(),
                                            b.builder.Constant(4_i), b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kSubtract);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -432,14 +432,14 @@
 TEST_F(IR_InstructionTest, CreateMultiply) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.Multiply(b.builder.ir.types.Get<type::I32>(),
                                            b.builder.Constant(4_i), b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kMultiply);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -459,14 +459,14 @@
 TEST_F(IR_InstructionTest, CreateDivide) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.Divide(b.builder.ir.types.Get<type::I32>(),
                                          b.builder.Constant(4_i), b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kDivide);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -486,14 +486,14 @@
 TEST_F(IR_InstructionTest, CreateModulo) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.Modulo(b.builder.ir.types.Get<type::I32>(),
                                          b.builder.Constant(4_i), b.builder.Constant(2_i));
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kModulo);
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
 
     ASSERT_TRUE(instr->LHS()->Is<Constant>());
     auto lhs = instr->LHS()->As<Constant>()->value;
@@ -513,7 +513,7 @@
 TEST_F(IR_InstructionTest, Binary_Usage) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.And(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
                                       b.builder.Constant(2_i));
 
@@ -537,7 +537,7 @@
 
     auto val = b.builder.Constant(4_i);
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr = b.builder.And(b.builder.ir.types.Get<type::I32>(), val, val);
 
     EXPECT_EQ(instr->GetKind(), Binary::Kind::kAnd);
diff --git a/src/tint/ir/bitcast.h b/src/tint/ir/bitcast.h
index 7dcb495..c7d9cb8 100644
--- a/src/tint/ir/bitcast.h
+++ b/src/tint/ir/bitcast.h
@@ -16,8 +16,6 @@
 #define SRC_TINT_IR_BITCAST_H_
 
 #include "src/tint/ir/instruction.h"
-#include "src/tint/symbol_table.h"
-#include "src/tint/type/type.h"
 #include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
diff --git a/src/tint/ir/bitcast_test.cc b/src/tint/ir/bitcast_test.cc
index b46c334..2e6ca9d 100644
--- a/src/tint/ir/bitcast_test.cc
+++ b/src/tint/ir/bitcast_test.cc
@@ -26,12 +26,12 @@
 TEST_F(IR_InstructionTest, Bitcast) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr =
         b.builder.Bitcast(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
 
-    ASSERT_TRUE(instr->Result()->Is<Temp>());
-    EXPECT_EQ(Temp::Id(42), instr->Result()->As<Temp>()->AsId());
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
     ASSERT_NE(instr->Result()->Type(), nullptr);
 
     ASSERT_TRUE(instr->Val()->Is<Constant>());
@@ -47,7 +47,7 @@
 TEST_F(IR_InstructionTest, Bitcast_Usage) {
     auto& b = CreateEmptyBuilder();
 
-    b.builder.next_temp_id = Temp::Id(42);
+    b.builder.next_runtime_id = Runtime::Id(42);
     const auto* instr =
         b.builder.Bitcast(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
 
diff --git a/src/tint/ir/builder.cc b/src/tint/ir/builder.cc
index a3d2a31..13ef345 100644
--- a/src/tint/ir/builder.cc
+++ b/src/tint/ir/builder.cc
@@ -93,12 +93,12 @@
     to->inbound_branches.Push(from);
 }
 
-Temp::Id Builder::AllocateTempId() {
-    return next_temp_id++;
+Runtime::Id Builder::AllocateRuntimeId() {
+    return next_runtime_id++;
 }
 
 Binary* Builder::CreateBinary(Binary::Kind kind, const type::Type* type, Value* lhs, Value* rhs) {
-    return ir.instructions.Create<ir::Binary>(kind, Temp(type), lhs, rhs);
+    return ir.instructions.Create<ir::Binary>(kind, Runtime(type), lhs, rhs);
 }
 
 Binary* Builder::And(const type::Type* type, Value* lhs, Value* rhs) {
@@ -173,30 +173,62 @@
     return CreateBinary(Binary::Kind::kModulo, type, lhs, rhs);
 }
 
+Unary* Builder::CreateUnary(Unary::Kind kind, const type::Type* type, Value* val) {
+    return ir.instructions.Create<ir::Unary>(kind, Runtime(type), val);
+}
+
+Unary* Builder::AddressOf(const type::Type* type, Value* val) {
+    return CreateUnary(Unary::Kind::kAddressOf, type, val);
+}
+
+Unary* Builder::Complement(const type::Type* type, Value* val) {
+    return CreateUnary(Unary::Kind::kComplement, type, val);
+}
+
+Unary* Builder::Indirection(const type::Type* type, Value* val) {
+    return CreateUnary(Unary::Kind::kIndirection, type, val);
+}
+
+Unary* Builder::Negation(const type::Type* type, Value* val) {
+    return CreateUnary(Unary::Kind::kNegation, type, val);
+}
+
+Unary* Builder::Not(const type::Type* type, Value* val) {
+    return CreateUnary(Unary::Kind::kNot, type, val);
+}
+
 ir::Bitcast* Builder::Bitcast(const type::Type* type, Value* val) {
-    return ir.instructions.Create<ir::Bitcast>(Temp(type), val);
+    return ir.instructions.Create<ir::Bitcast>(Runtime(type), val);
+}
+
+ir::Discard* Builder::Discard() {
+    return ir.instructions.Create<ir::Discard>(Runtime(ir.types.Get<type::Void>()));
 }
 
 ir::UserCall* Builder::UserCall(const type::Type* type,
                                 Symbol name,
                                 utils::VectorRef<Value*> args) {
-    return ir.instructions.Create<ir::UserCall>(Temp(type), name, std::move(args));
+    return ir.instructions.Create<ir::UserCall>(Runtime(type), name, std::move(args));
 }
 
 ir::Convert* Builder::Convert(const type::Type* to,
                               const type::Type* from,
                               utils::VectorRef<Value*> args) {
-    return ir.instructions.Create<ir::Convert>(Temp(to), from, std::move(args));
+    return ir.instructions.Create<ir::Convert>(Runtime(to), from, std::move(args));
 }
 
 ir::Construct* Builder::Construct(const type::Type* to, utils::VectorRef<Value*> args) {
-    return ir.instructions.Create<ir::Construct>(Temp(to), std::move(args));
+    return ir.instructions.Create<ir::Construct>(Runtime(to), std::move(args));
 }
 
 ir::Builtin* Builder::Builtin(const type::Type* type,
                               builtin::Function func,
                               utils::VectorRef<Value*> args) {
-    return ir.instructions.Create<ir::Builtin>(Temp(type), func, args);
+    return ir.instructions.Create<ir::Builtin>(Runtime(type), func, args);
+}
+
+ir::Store* Builder::Store(Value* to, Value* from) {
+    return ir.instructions.Create<ir::Store>(to, from);
 }
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder.h b/src/tint/ir/builder.h
index b090bdf..3fc78d2 100644
--- a/src/tint/ir/builder.h
+++ b/src/tint/ir/builder.h
@@ -24,13 +24,16 @@
 #include "src/tint/ir/constant.h"
 #include "src/tint/ir/construct.h"
 #include "src/tint/ir/convert.h"
+#include "src/tint/ir/discard.h"
 #include "src/tint/ir/function.h"
 #include "src/tint/ir/if.h"
 #include "src/tint/ir/loop.h"
 #include "src/tint/ir/module.h"
+#include "src/tint/ir/runtime.h"
+#include "src/tint/ir/store.h"
 #include "src/tint/ir/switch.h"
-#include "src/tint/ir/temp.h"
 #include "src/tint/ir/terminator.h"
+#include "src/tint/ir/unary.h"
 #include "src/tint/ir/user_call.h"
 #include "src/tint/ir/value.h"
 #include "src/tint/type/bool.h"
@@ -38,6 +41,7 @@
 #include "src/tint/type/f32.h"
 #include "src/tint/type/i32.h"
 #include "src/tint/type/u32.h"
+#include "src/tint/type/void.h"
 
 namespace tint::ir {
 
@@ -137,11 +141,11 @@
         return Constant(create<constant::Scalar<bool>>(ir.types.Get<type::Bool>(), v));
     }
 
-    /// Creates a new Temporary
+    /// Creates a new Runtime value
     /// @param type the type of the temporary
     /// @returns the new temporary
-    ir::Temp* Temp(const type::Type* type) {
-        return ir.values.Create<ir::Temp>(type, AllocateTempId());
+    ir::Runtime* Runtime(const type::Type* type) {
+        return ir.values.Create<ir::Runtime>(type, AllocateRuntimeId());
     }
 
     /// Creates an op for `lhs kind rhs`
@@ -278,12 +282,53 @@
     /// @returns the operation
     Binary* Modulo(const type::Type* type, Value* lhs, Value* rhs);
 
+    /// Creates an op for `kind val`
+    /// @param kind the kind of operation
+    /// @param type the result type of the binary expression
+    /// @param val the value of the operation
+    /// @returns the operation
+    Unary* CreateUnary(Unary::Kind kind, const type::Type* type, Value* val);
+
+    /// Creates an AddressOf operation
+    /// @param type the result type of the expression
+    /// @param val the value
+    /// @returns the operation
+    Unary* AddressOf(const type::Type* type, Value* val);
+
+    /// Creates a Complement operation
+    /// @param type the result type of the expression
+    /// @param val the value
+    /// @returns the operation
+    Unary* Complement(const type::Type* type, Value* val);
+
+    /// Creates an Indirection operation
+    /// @param type the result type of the expression
+    /// @param val the value
+    /// @returns the operation
+    Unary* Indirection(const type::Type* type, Value* val);
+
+    /// Creates a Negation operation
+    /// @param type the result type of the expression
+    /// @param val the value
+    /// @returns the operation
+    Unary* Negation(const type::Type* type, Value* val);
+
+    /// Creates a Not operation
+    /// @param type the result type of the expression
+    /// @param val the value
+    /// @returns the operation
+    Unary* Not(const type::Type* type, Value* val);
+
     /// Creates a bitcast instruction
     /// @param type the result type of the bitcast
     /// @param val the value being bitcast
     /// @returns the instruction
     ir::Bitcast* Bitcast(const type::Type* type, Value* val);
 
+    /// Creates a discard instruction
+    /// @returns the instruction
+    ir::Discard* Discard();
+
     /// Creates a user function call instruction
     /// @param type the return type of the call
     /// @param name the name of the function being called
@@ -315,14 +360,20 @@
                          builtin::Function func,
                          utils::VectorRef<Value*> args);
 
-    /// @returns a unique temp id
-    Temp::Id AllocateTempId();
+    /// Creates an store instruction
+    /// @param to the expression being stored too
+    /// @param from the expression being stored
+    /// @returns the instruction
+    ir::Store* Store(Value* to, Value* from);
+
+    /// @returns a unique runtime id
+    Runtime::Id AllocateRuntimeId();
 
     /// The IR module.
     Module ir;
 
     /// The next temporary number to allocate
-    Temp::Id next_temp_id = 1;
+    Runtime::Id next_runtime_id = 1;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder_impl.cc b/src/tint/ir/builder_impl.cc
index c6c6ed0..1f0f79a 100644
--- a/src/tint/ir/builder_impl.cc
+++ b/src/tint/ir/builder_impl.cc
@@ -17,6 +17,7 @@
 #include <iostream>
 
 #include "src/tint/ast/alias.h"
+#include "src/tint/ast/assignment_statement.h"
 #include "src/tint/ast/binary_expression.h"
 #include "src/tint/ast/bitcast_expression.h"
 #include "src/tint/ast/block_statement.h"
@@ -25,8 +26,11 @@
 #include "src/tint/ast/break_statement.h"
 #include "src/tint/ast/call_expression.h"
 #include "src/tint/ast/call_statement.h"
+#include "src/tint/ast/compound_assignment_statement.h"
+#include "src/tint/ast/const.h"
 #include "src/tint/ast/const_assert.h"
 #include "src/tint/ast/continue_statement.h"
+#include "src/tint/ast/discard_statement.h"
 #include "src/tint/ast/float_literal_expression.h"
 #include "src/tint/ast/for_loop_statement.h"
 #include "src/tint/ast/function.h"
@@ -45,14 +49,17 @@
 #include "src/tint/ast/struct_member_size_attribute.h"
 #include "src/tint/ast/switch_statement.h"
 #include "src/tint/ast/templated_identifier.h"
+#include "src/tint/ast/unary_op_expression.h"
 #include "src/tint/ast/variable_decl_statement.h"
 #include "src/tint/ast/while_statement.h"
 #include "src/tint/ir/function.h"
 #include "src/tint/ir/if.h"
 #include "src/tint/ir/loop.h"
 #include "src/tint/ir/module.h"
+#include "src/tint/ir/store.h"
 #include "src/tint/ir/switch.h"
 #include "src/tint/ir/terminator.h"
+#include "src/tint/ir/value.h"
 #include "src/tint/program.h"
 #include "src/tint/sem/builtin.h"
 #include "src/tint/sem/call.h"
@@ -153,16 +160,14 @@
     auto* sem = program_->Sem().Module();
 
     for (auto* decl : sem->DependencyOrderedDeclarations()) {
-        bool ok = tint::Switch(
+        tint::Switch(
             decl,  //
             [&](const ast::Struct*) {
                 // Will be encoded into the `type::Struct` when used. We will then hoist all
                 // used structs up to module scope when converting IR.
-                return true;
             },
             [&](const ast::Alias*) {
                 // Folded away and doesn't appear in the IR.
-                return true;
             },
             // [&](const ast::Variable* var) {
             // TODO(dsinclair): Implement
@@ -174,21 +179,19 @@
             // },
             [&](const ast::ConstAssert*) {
                 // Evaluated by the resolver, drop from the IR.
-                return true;
             },
             [&](Default) {
                 add_error(decl->source, "unknown type: " + std::string(decl->TypeInfo().name));
-                return true;
             });
-        if (!ok) {
-            return utils::Failure;
-        }
+    }
+    if (!diagnostics_.empty()) {
+        return utils::Failure;
     }
 
     return ResultType{std::move(builder.ir)};
 }
 
-bool BuilderImpl::EmitFunction(const ast::Function* ast_func) {
+void BuilderImpl::EmitFunction(const ast::Function* ast_func) {
     // The flow stack should have been emptied when the previous function finished building.
     TINT_ASSERT(IR, flow_stack.IsEmpty());
 
@@ -207,9 +210,7 @@
         FlowStackScope scope(this, ir_func);
 
         current_flow_block = ir_func->start_target;
-        if (!EmitStatements(ast_func->body->statements)) {
-            return false;
-        }
+        EmitStatements(ast_func->body->statements);
 
         // TODO(dsinclair): Store return type and attributes
         // TODO(dsinclair): Store parameters
@@ -223,15 +224,11 @@
     TINT_ASSERT(IR, flow_stack.IsEmpty());
     current_flow_block = nullptr;
     current_function_ = nullptr;
-
-    return true;
 }
 
-bool BuilderImpl::EmitStatements(utils::VectorRef<const ast::Statement*> stmts) {
+void BuilderImpl::EmitStatements(utils::VectorRef<const ast::Statement*> stmts) {
     for (auto* s : stmts) {
-        if (!EmitStatement(s)) {
-            return false;
-        }
+        EmitStatement(s);
 
         // If the current flow block has a branch target then the rest of the statements in this
         // block are dead code. Skip them.
@@ -239,59 +236,141 @@
             break;
         }
     }
-    return true;
 }
 
-bool BuilderImpl::EmitStatement(const ast::Statement* stmt) {
-    return tint::Switch(
-        stmt,
-        // [&](const ast::AssignmentStatement* a) {
-        // TODO(dsinclair): Implement
-        // },
-        [&](const ast::BlockStatement* b) { return EmitBlock(b); },
-        [&](const ast::BreakStatement* b) { return EmitBreak(b); },
-        [&](const ast::BreakIfStatement* b) { return EmitBreakIf(b); },
-        [&](const ast::CallStatement* c) { return EmitCall(c); },
-        // [&](const ast::CompoundAssignmentStatement* c) {
-        // TODO(dsinclair): Implement
-        // },
-        [&](const ast::ContinueStatement* c) { return EmitContinue(c); },
-        // [&](const ast::DiscardStatement* d) {
-        // TODO(dsinclair): Implement
-        // },
-        [&](const ast::IfStatement* i) { return EmitIf(i); },
-        [&](const ast::LoopStatement* l) { return EmitLoop(l); },
-        [&](const ast::ForLoopStatement* l) { return EmitForLoop(l); },
-        [&](const ast::WhileStatement* l) { return EmitWhile(l); },
-        [&](const ast::ReturnStatement* r) { return EmitReturn(r); },
-        [&](const ast::SwitchStatement* s) { return EmitSwitch(s); },
-        [&](const ast::VariableDeclStatement* v) { return EmitVariable(v->variable); },
+void BuilderImpl::EmitStatement(const ast::Statement* stmt) {
+    tint::Switch(
+        stmt,  //
+        [&](const ast::AssignmentStatement* a) { EmitAssignment(a); },
+        [&](const ast::BlockStatement* b) { EmitBlock(b); },
+        [&](const ast::BreakStatement* b) { EmitBreak(b); },
+        [&](const ast::BreakIfStatement* b) { EmitBreakIf(b); },
+        [&](const ast::CallStatement* c) { EmitCall(c); },
+        [&](const ast::CompoundAssignmentStatement* c) { EmitCompoundAssignment(c); },
+        [&](const ast::ContinueStatement* c) { EmitContinue(c); },
+        [&](const ast::DiscardStatement* d) { EmitDiscard(d); },
+        [&](const ast::IfStatement* i) { EmitIf(i); },
+        [&](const ast::LoopStatement* l) { EmitLoop(l); },
+        [&](const ast::ForLoopStatement* l) { EmitForLoop(l); },
+        [&](const ast::WhileStatement* l) { EmitWhile(l); },
+        [&](const ast::ReturnStatement* r) { EmitReturn(r); },
+        [&](const ast::SwitchStatement* s) { EmitSwitch(s); },
+        [&](const ast::VariableDeclStatement* v) { EmitVariable(v->variable); },
         [&](const ast::ConstAssert*) {
-            return true;  // Not emitted
+            // Not emitted
         },
         [&](Default) {
             add_error(stmt->source,
                       "unknown statement type: " + std::string(stmt->TypeInfo().name));
-            // TODO(dsinclair): This should return `false`, switch back when all
-            // the cases are handled.
-            return true;
         });
 }
 
-bool BuilderImpl::EmitBlock(const ast::BlockStatement* block) {
+void BuilderImpl::EmitAssignment(const ast::AssignmentStatement* stmt) {
+    auto lhs = EmitExpression(stmt->lhs);
+    if (!lhs) {
+        return;
+    }
+
+    auto rhs = EmitExpression(stmt->rhs);
+    if (!rhs) {
+        return;
+    }
+    auto store = builder.Store(lhs.Get(), rhs.Get());
+    current_flow_block->instructions.Push(store);
+}
+
+void BuilderImpl::EmitCompoundAssignment(const ast::CompoundAssignmentStatement* stmt) {
+    auto lhs = EmitExpression(stmt->lhs);
+    if (!lhs) {
+        return;
+    }
+
+    auto rhs = EmitExpression(stmt->rhs);
+    if (!rhs) {
+        return;
+    }
+
+    auto* ty = lhs.Get()->Type();
+    Binary* instr = nullptr;
+    switch (stmt->op) {
+        case ast::BinaryOp::kAnd:
+            instr = builder.And(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kOr:
+            instr = builder.Or(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kXor:
+            instr = builder.Xor(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kLogicalAnd:
+            instr = builder.LogicalAnd(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kLogicalOr:
+            instr = builder.LogicalOr(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kEqual:
+            instr = builder.Equal(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kNotEqual:
+            instr = builder.NotEqual(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kLessThan:
+            instr = builder.LessThan(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kGreaterThan:
+            instr = builder.GreaterThan(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kLessThanEqual:
+            instr = builder.LessThanEqual(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kGreaterThanEqual:
+            instr = builder.GreaterThanEqual(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kShiftLeft:
+            instr = builder.ShiftLeft(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kShiftRight:
+            instr = builder.ShiftRight(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kAdd:
+            instr = builder.Add(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kSubtract:
+            instr = builder.Subtract(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kMultiply:
+            instr = builder.Multiply(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kDivide:
+            instr = builder.Divide(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kModulo:
+            instr = builder.Modulo(ty, lhs.Get(), rhs.Get());
+            break;
+        case ast::BinaryOp::kNone:
+            TINT_ICE(IR, diagnostics_) << "missing binary operand type";
+            return;
+    }
+    current_flow_block->instructions.Push(instr);
+
+    auto store = builder.Store(lhs.Get(), instr->Result());
+    current_flow_block->instructions.Push(store);
+}
+
+void BuilderImpl::EmitBlock(const ast::BlockStatement* block) {
     // Note, this doesn't need to emit a Block as the current block flow node should be
     // sufficient as the blocks all get flattened. Each flow control node will inject the basic
     // blocks it requires.
-    return EmitStatements(block->statements);
+    EmitStatements(block->statements);
 }
 
-bool BuilderImpl::EmitIf(const ast::IfStatement* stmt) {
+void BuilderImpl::EmitIf(const ast::IfStatement* stmt) {
     auto* if_node = builder.CreateIf();
 
     // Emit the if condition into the end of the preceding block
     auto reg = EmitExpression(stmt->condition);
     if (!reg) {
-        return false;
+        return;
     }
     if_node->condition = reg.Get();
 
@@ -303,16 +382,16 @@
         FlowStackScope scope(this, if_node);
 
         current_flow_block = if_node->true_.target->As<Block>();
-        if (!EmitStatement(stmt->body)) {
-            return false;
-        }
+        EmitStatement(stmt->body);
+
         // If the true branch did not execute control flow, then go to the merge target
         BranchToIfNeeded(if_node->merge.target);
 
         current_flow_block = if_node->false_.target->As<Block>();
-        if (stmt->else_statement && !EmitStatement(stmt->else_statement)) {
-            return false;
+        if (stmt->else_statement) {
+            EmitStatement(stmt->else_statement);
         }
+
         // If the false branch did not execute control flow, then go to the merge target
         BranchToIfNeeded(if_node->merge.target);
     }
@@ -324,11 +403,9 @@
     if (IsConnected(if_node->merge.target)) {
         current_flow_block = if_node->merge.target->As<Block>();
     }
-
-    return true;
 }
 
-bool BuilderImpl::EmitLoop(const ast::LoopStatement* stmt) {
+void BuilderImpl::EmitLoop(const ast::LoopStatement* stmt) {
     auto* loop_node = builder.CreateLoop();
 
     BranchTo(loop_node);
@@ -339,18 +416,14 @@
         FlowStackScope scope(this, loop_node);
 
         current_flow_block = loop_node->start.target->As<Block>();
-        if (!EmitStatement(stmt->body)) {
-            return false;
-        }
+        EmitStatement(stmt->body);
 
         // The current block didn't `break`, `return` or `continue`, go to the continuing block.
         BranchToIfNeeded(loop_node->continuing.target);
 
         current_flow_block = loop_node->continuing.target->As<Block>();
         if (stmt->continuing) {
-            if (!EmitStatement(stmt->continuing)) {
-                return false;
-            }
+            EmitStatement(stmt->continuing);
         }
 
         // Branch back to the start node if the continue target didn't branch out already
@@ -363,10 +436,9 @@
     if (!IsConnected(loop_node->merge.target)) {
         current_flow_block = nullptr;
     }
-    return true;
 }
 
-bool BuilderImpl::EmitWhile(const ast::WhileStatement* stmt) {
+void BuilderImpl::EmitWhile(const ast::WhileStatement* stmt) {
     auto* loop_node = builder.CreateLoop();
     // Continue is always empty, just go back to the start
     TINT_ASSERT(IR, loop_node->continuing.target->Is<Block>());
@@ -385,7 +457,7 @@
         // Emit the while condition into the start target of the loop
         auto reg = EmitExpression(stmt->condition);
         if (!reg) {
-            return false;
+            return;
         }
 
         // Create an `if (cond) {} else {break;}` control flow
@@ -400,19 +472,16 @@
         BranchTo(if_node);
 
         current_flow_block = if_node->merge.target->As<Block>();
-        if (!EmitStatement(stmt->body)) {
-            return false;
-        }
+        EmitStatement(stmt->body);
 
         BranchToIfNeeded(loop_node->continuing.target);
     }
     // The while loop always has a path to the merge target as the break statement comes before
     // anything inside the loop.
     current_flow_block = loop_node->merge.target->As<Block>();
-    return true;
 }
 
-bool BuilderImpl::EmitForLoop(const ast::ForLoopStatement* stmt) {
+void BuilderImpl::EmitForLoop(const ast::ForLoopStatement* stmt) {
     auto* loop_node = builder.CreateLoop();
     TINT_ASSERT(IR, loop_node->continuing.target->Is<Block>());
     builder.Branch(loop_node->continuing.target->As<Block>(), loop_node->start.target,
@@ -420,9 +489,7 @@
 
     if (stmt->initializer) {
         // Emit the for initializer before branching to the loop
-        if (!EmitStatement(stmt->initializer)) {
-            return false;
-        }
+        EmitStatement(stmt->initializer);
     }
 
     BranchTo(loop_node);
@@ -438,7 +505,7 @@
             // Emit the condition into the target target of the loop
             auto reg = EmitExpression(stmt->condition);
             if (!reg) {
-                return false;
+                return;
             }
 
             // Create an `if (cond) {} else {break;}` control flow
@@ -455,32 +522,26 @@
             current_flow_block = if_node->merge.target->As<Block>();
         }
 
-        if (!EmitStatement(stmt->body)) {
-            return false;
-        }
-
+        EmitStatement(stmt->body);
         BranchToIfNeeded(loop_node->continuing.target);
 
         if (stmt->continuing) {
             current_flow_block = loop_node->continuing.target->As<Block>();
-            if (!EmitStatement(stmt->continuing)) {
-                return false;
-            }
+            EmitStatement(stmt->continuing);
         }
     }
     // The while loop always has a path to the merge target as the break statement comes before
     // anything inside the loop.
     current_flow_block = loop_node->merge.target->As<Block>();
-    return true;
 }
 
-bool BuilderImpl::EmitSwitch(const ast::SwitchStatement* stmt) {
+void BuilderImpl::EmitSwitch(const ast::SwitchStatement* stmt) {
     auto* switch_node = builder.CreateSwitch();
 
     // Emit the condition into the preceding block
     auto reg = EmitExpression(stmt->condition);
     if (!reg) {
-        return false;
+        return;
     }
     switch_node->condition = reg.Get();
 
@@ -503,9 +564,7 @@
             }
 
             current_flow_block = builder.CreateCase(switch_node, selectors);
-            if (!EmitStatement(c->Body()->Declaration())) {
-                return false;
-            }
+            EmitStatement(c->Body()->Declaration());
             BranchToIfNeeded(switch_node->merge.target);
         }
     }
@@ -514,25 +573,22 @@
     if (IsConnected(switch_node->merge.target)) {
         current_flow_block = switch_node->merge.target->As<Block>();
     }
-
-    return true;
 }
 
-bool BuilderImpl::EmitReturn(const ast::ReturnStatement* stmt) {
+void BuilderImpl::EmitReturn(const ast::ReturnStatement* stmt) {
     utils::Vector<Value*, 1> ret_value;
     if (stmt->value) {
         auto ret = EmitExpression(stmt->value);
         if (!ret) {
-            return false;
+            return;
         }
         ret_value.Push(ret.Get());
     }
 
     BranchTo(current_function_->end_target, std::move(ret_value));
-    return true;
 }
 
-bool BuilderImpl::EmitBreak(const ast::BreakStatement*) {
+void BuilderImpl::EmitBreak(const ast::BreakStatement*) {
     auto* current_control = FindEnclosingControl(ControlFlags::kNone);
     TINT_ASSERT(IR, current_control);
 
@@ -542,13 +598,10 @@
         BranchTo(s->merge.target);
     } else {
         TINT_UNREACHABLE(IR, diagnostics_);
-        return false;
     }
-
-    return true;
 }
 
-bool BuilderImpl::EmitContinue(const ast::ContinueStatement*) {
+void BuilderImpl::EmitContinue(const ast::ContinueStatement*) {
     auto* current_control = FindEnclosingControl(ControlFlags::kExcludeSwitch);
     TINT_ASSERT(IR, current_control);
 
@@ -557,17 +610,24 @@
     } else {
         TINT_UNREACHABLE(IR, diagnostics_);
     }
-
-    return true;
 }
 
-bool BuilderImpl::EmitBreakIf(const ast::BreakIfStatement* stmt) {
+// Discard is being treated as an instruction. The semantics in WGSL is demote_to_helper, so the
+// code has to continue as before it just predicates writes. If WGSL grows some kind of terminating
+// discard that would probably make sense as a FlowNode but would then require figuring out the
+// multi-level exit that is triggered.
+void BuilderImpl::EmitDiscard(const ast::DiscardStatement*) {
+    auto* instr = builder.Discard();
+    current_flow_block->instructions.Push(instr);
+}
+
+void BuilderImpl::EmitBreakIf(const ast::BreakIfStatement* stmt) {
     auto* if_node = builder.CreateIf();
 
     // Emit the break-if condition into the end of the preceding block
     auto reg = EmitExpression(stmt->condition);
     if (!reg) {
-        return false;
+        return;
     }
     if_node->condition = reg.Get();
 
@@ -592,8 +652,6 @@
     // The `break-if` has to be the last item in the continuing block. The false branch of the
     // `break-if` will always take us back to the start of the loop.
     BranchTo(loop->start.target);
-
-    return true;
 }
 
 utils::Result<Value*> BuilderImpl::EmitExpression(const ast::Expression* expr) {
@@ -615,20 +673,15 @@
         // [&](const ast::PhonyExpression*) {
         // TODO(dsinclair): Implement. The call may have side effects so has to be made.
         // },
-        // [&](const ast::UnaryOpExpression* u) {
-        // TODO(dsinclair): Implement
-        // },
+        [&](const ast::UnaryOpExpression* u) { return EmitUnary(u); },
         [&](Default) {
             add_error(expr->source,
                       "unknown expression type: " + std::string(expr->TypeInfo().name));
-            // TODO(dsinclair): This should return utils::Failure; Switch back
-            // once all the above cases are handled.
-            auto* v = builder.ir.types.Get<type::Void>();
-            return builder.Temp(v);
+            return utils::Failure;
         });
 }
 
-bool BuilderImpl::EmitVariable(const ast::Variable* var) {
+void BuilderImpl::EmitVariable(const ast::Variable* var) {
     return tint::Switch(  //
         var,
         // [&](const ast::Var* var) {
@@ -641,20 +694,53 @@
             add_error(var->source,
                       "found an `Override` variable. The SubstituteOverrides "
                       "transform must be run before converting to IR");
-            return false;
         },
-        // [&](const ast::Const* c) {
-        // TODO(dsinclair): Implement
-        // },
+        [&](const ast::Const*) {
+            // Skip. This should be handled by const-eval already, so the const will be a
+            // `constant::` value at the usage sites. Can just ignore the `const` variable as it
+            // should never be used.
+            //
+            // TODO(dsinclair): Probably want to store the const variable somewhere and then in
+            // identifier expression log an error if we ever see a const identifier. Add this when
+            // identifiers and variables are supported.
+        },
         [&](Default) {
             add_error(var->source, "unknown variable: " + std::string(var->TypeInfo().name));
-
-            // TODO(dsinclair): This should return `false`, switch back when all
-            // the cases are handled.
-            return true;
         });
 }
 
+utils::Result<Value*> BuilderImpl::EmitUnary(const ast::UnaryOpExpression* expr) {
+    auto val = EmitExpression(expr->expr);
+    if (!val) {
+        return utils::Failure;
+    }
+
+    auto* sem = program_->Sem().Get(expr);
+    auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
+
+    Unary* instr = nullptr;
+    switch (expr->op) {
+        case ast::UnaryOp::kAddressOf:
+            instr = builder.AddressOf(ty, val.Get());
+            break;
+        case ast::UnaryOp::kComplement:
+            instr = builder.Complement(ty, val.Get());
+            break;
+        case ast::UnaryOp::kIndirection:
+            instr = builder.Indirection(ty, val.Get());
+            break;
+        case ast::UnaryOp::kNegation:
+            instr = builder.Negation(ty, val.Get());
+            break;
+        case ast::UnaryOp::kNot:
+            instr = builder.Not(ty, val.Get());
+            break;
+    }
+
+    current_flow_block->instructions.Push(instr);
+    return instr->Result();
+}
+
 utils::Result<Value*> BuilderImpl::EmitBinary(const ast::BinaryExpression* expr) {
     auto lhs = EmitExpression(expr->lhs);
     if (!lhs) {
@@ -748,8 +834,8 @@
     return instr->Result();
 }
 
-utils::Result<Value*> BuilderImpl::EmitCall(const ast::CallStatement* stmt) {
-    return EmitCall(stmt->expr);
+void BuilderImpl::EmitCall(const ast::CallStatement* stmt) {
+    (void)EmitCall(stmt->expr);
 }
 
 utils::Result<Value*> BuilderImpl::EmitCall(const ast::CallExpression* expr) {
@@ -830,17 +916,14 @@
     return builder.Constant(cv);
 }
 
-bool BuilderImpl::EmitAttributes(utils::VectorRef<const ast::Attribute*> attrs) {
+void BuilderImpl::EmitAttributes(utils::VectorRef<const ast::Attribute*> attrs) {
     for (auto* attr : attrs) {
-        if (!EmitAttribute(attr)) {
-            return false;
-        }
+        EmitAttribute(attr);
     }
-    return true;
 }
 
-bool BuilderImpl::EmitAttribute(const ast::Attribute* attr) {
-    return tint::Switch(  //
+void BuilderImpl::EmitAttribute(const ast::Attribute* attr) {
+    tint::Switch(  //
         attr,
         // [&](const ast::WorkgroupAttribute* wg) {
         // TODO(dsinclair): Implement
@@ -873,17 +956,14 @@
             add_error(attr->source,
                       "found an `Id` attribute. The SubstituteOverrides transform "
                       "must be run before converting to IR");
-            return false;
         },
         [&](const ast::StructMemberSizeAttribute*) {
             TINT_ICE(IR, diagnostics_)
                 << "StructMemberSizeAttribute encountered during IR conversion";
-            return false;
         },
         [&](const ast::StructMemberAlignAttribute*) {
             TINT_ICE(IR, diagnostics_)
                 << "StructMemberAlignAttribute encountered during IR conversion";
-            return false;
         },
         // [&](const ast::StrideAttribute* s) {
         // TODO(dsinclair): Implement
@@ -893,7 +973,6 @@
         // },
         [&](Default) {
             add_error(attr->source, "unknown attribute: " + std::string(attr->TypeInfo().name));
-            return false;
         });
 }
 
diff --git a/src/tint/ir/builder_impl.h b/src/tint/ir/builder_impl.h
index 2e38ef7..7b18492 100644
--- a/src/tint/ir/builder_impl.h
+++ b/src/tint/ir/builder_impl.h
@@ -34,6 +34,7 @@
 }  // namespace tint
 namespace tint::ast {
 class Attribute;
+class AssignmentStatement;
 class BinaryExpression;
 class BitcastExpression;
 class BlockStatement;
@@ -41,7 +42,9 @@
 class BreakStatement;
 class CallExpression;
 class CallStatement;
+class CompoundAssignmentStatement;
 class ContinueStatement;
+class DiscardStatement;
 class Expression;
 class ForLoopStatement;
 class Function;
@@ -52,6 +55,7 @@
 class ReturnStatement;
 class Statement;
 class SwitchStatement;
+class UnaryOpExpression;
 class WhileStatement;
 class Variable;
 }  // namespace tint::ast
@@ -82,73 +86,71 @@
     /// @returns true on success, false otherwise
     utils::Result<Module> Build();
 
-    /// @returns the error
-    std::string error() const { return diagnostics_.str(); }
+    /// @returns the diagnostics
+    diag::List Diagnostics() const { return diagnostics_; }
 
     /// Emits a function to the IR.
     /// @param func the function to emit
-    /// @returns true if successful, false otherwise
-    bool EmitFunction(const ast::Function* func);
+    void EmitFunction(const ast::Function* func);
 
     /// Emits a set of statements to the IR.
     /// @param stmts the statements to emit
-    /// @returns true if successful, false otherwise.
-    bool EmitStatements(utils::VectorRef<const ast::Statement*> stmts);
+    void EmitStatements(utils::VectorRef<const ast::Statement*> stmts);
 
     /// Emits a statement to the IR
     /// @param stmt the statment to emit
-    /// @returns true on success, false otherwise.
-    bool EmitStatement(const ast::Statement* stmt);
+    void EmitStatement(const ast::Statement* stmt);
 
     /// Emits a block statement to the IR.
     /// @param block the block to emit
-    /// @returns true if successful, false otherwise.
-    bool EmitBlock(const ast::BlockStatement* block);
+    void EmitBlock(const ast::BlockStatement* block);
 
     /// Emits an if control node to the IR.
     /// @param stmt the if statement
-    /// @returns true if successful, false otherwise.
-    bool EmitIf(const ast::IfStatement* stmt);
+    void EmitIf(const ast::IfStatement* stmt);
 
     /// Emits a return node to the IR.
     /// @param stmt the return AST statement
-    /// @returns true if successful, false otherwise.
-    bool EmitReturn(const ast::ReturnStatement* stmt);
+    void EmitReturn(const ast::ReturnStatement* stmt);
 
     /// Emits a loop control node to the IR.
     /// @param stmt the loop statement
-    /// @returns true if successful, false otherwise.
-    bool EmitLoop(const ast::LoopStatement* stmt);
+    void EmitLoop(const ast::LoopStatement* stmt);
 
     /// Emits a loop control node to the IR.
     /// @param stmt the while statement
-    /// @returns true if successful, false otherwise.
-    bool EmitWhile(const ast::WhileStatement* stmt);
+    void EmitWhile(const ast::WhileStatement* stmt);
 
     /// Emits a loop control node to the IR.
     /// @param stmt the for loop statement
-    /// @returns true if successful, false otherwise.
-    bool EmitForLoop(const ast::ForLoopStatement* stmt);
+    void EmitForLoop(const ast::ForLoopStatement* stmt);
 
     /// Emits a switch statement
     /// @param stmt the switch statement
-    /// @returns true if successful, false otherwise.
-    bool EmitSwitch(const ast::SwitchStatement* stmt);
+    void EmitSwitch(const ast::SwitchStatement* stmt);
 
     /// Emits a break statement
     /// @param stmt the break statement
-    /// @returns true if successful, false otherwise.
-    bool EmitBreak(const ast::BreakStatement* stmt);
+    void EmitBreak(const ast::BreakStatement* stmt);
 
     /// Emits a continue statement
     /// @param stmt the continue statement
-    /// @returns true if successful, false otherwise.
-    bool EmitContinue(const ast::ContinueStatement* stmt);
+    void EmitContinue(const ast::ContinueStatement* stmt);
+
+    /// Emits a discard statement
+    void EmitDiscard(const ast::DiscardStatement*);
 
     /// Emits a break-if statement
     /// @param stmt the break-if statement
-    /// @returns true if successful, false otherwise.
-    bool EmitBreakIf(const ast::BreakIfStatement* stmt);
+    void EmitBreakIf(const ast::BreakIfStatement* stmt);
+
+    /// Emits an assignment statement
+    /// @param stmt the statement
+    void EmitAssignment(const ast::AssignmentStatement* stmt);
+
+    /// Emits a compound assignment statement
+    /// @param stmt the statement
+    void EmitCompoundAssignment(const ast::CompoundAssignmentStatement* stmt);
 
     /// Emits an expression
     /// @param expr the expression to emit
@@ -157,8 +159,12 @@
 
     /// Emits a variable
     /// @param var the variable to emit
-    /// @returns true if successful, false otherwise
-    bool EmitVariable(const ast::Variable* var);
+    void EmitVariable(const ast::Variable* var);
+
+    /// Emits a Unary expression
+    /// @param expr the unary expression
+    /// @returns the value storing the result if successful, utils::Failure otherwise
+    utils::Result<Value*> EmitUnary(const ast::UnaryOpExpression* expr);
 
     /// Emits a binary expression
     /// @param expr the binary expression
@@ -172,8 +178,7 @@
 
     /// Emits a call expression
     /// @param stmt the call statement
-    /// @returns the value storing the result if successful, utils::Failure otherwise
-    utils::Result<Value*> EmitCall(const ast::CallStatement* stmt);
+    void EmitCall(const ast::CallStatement* stmt);
 
     /// Emits a call expression
     /// @param expr the call expression
@@ -187,13 +192,11 @@
 
     /// Emits a set of attributes
     /// @param attrs the attributes to emit
-    /// @returns true if successful, false otherwise
-    bool EmitAttributes(utils::VectorRef<const ast::Attribute*> attrs);
+    void EmitAttributes(utils::VectorRef<const ast::Attribute*> attrs);
 
     /// Emits an attribute
     /// @param attr the attribute to emit
-    /// @returns true if successful, false otherwise
-    bool EmitAttribute(const ast::Attribute* attr);
+    void EmitAttribute(const ast::Attribute* attr);
 
     /// Retrieve the IR Flow node for a given AST node.
     /// @param n the node to lookup
diff --git a/src/tint/ir/builder_impl_test.cc b/src/tint/ir/builder_impl_test.cc
index 4e02875..cd6bf0b 100644
--- a/src/tint/ir/builder_impl_test.cc
+++ b/src/tint/ir/builder_impl_test.cc
@@ -14,6 +14,7 @@
 
 #include "src/tint/ir/test_helper.h"
 
+#include "gmock/gmock.h"
 #include "src/tint/ast/case_selector.h"
 #include "src/tint/ast/int_literal_expression.h"
 #include "src/tint/constant/scalar.h"
@@ -1472,7 +1473,7 @@
 
     auto& b = CreateBuilder();
     auto r = b.EmitLiteral(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
 
     ASSERT_TRUE(r.Get()->Is<Constant>());
     auto* val = r.Get()->As<Constant>()->value;
@@ -1486,7 +1487,7 @@
 
     auto& b = CreateBuilder();
     auto r = b.EmitLiteral(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
 
     ASSERT_TRUE(r.Get()->Is<Constant>());
     auto* val = r.Get()->As<Constant>()->value;
@@ -1500,7 +1501,7 @@
 
     auto& b = CreateBuilder();
     auto r = b.EmitLiteral(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
 
     ASSERT_TRUE(r.Get()->Is<Constant>());
     auto* val = r.Get()->As<Constant>()->value;
@@ -1515,7 +1516,7 @@
 
     auto& b = CreateBuilder();
     auto r = b.EmitLiteral(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
 
     ASSERT_TRUE(r.Get()->Is<Constant>());
     auto* val = r.Get()->As<Constant>()->value;
@@ -1529,7 +1530,7 @@
 
     auto& b = CreateBuilder();
     auto r = b.EmitLiteral(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
 
     ASSERT_TRUE(r.Get()->Is<Constant>());
     auto* val = r.Get()->As<Constant>()->value;
@@ -1543,7 +1544,7 @@
 
     auto& b = CreateBuilder();
     auto r = b.EmitLiteral(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
 
     ASSERT_TRUE(r.Get()->Is<Constant>());
     auto* val = r.Get()->As<Constant>()->value;
@@ -1558,7 +1559,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1573,7 +1575,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1588,7 +1591,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1603,7 +1607,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1618,7 +1623,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1633,7 +1639,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1648,7 +1655,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1663,7 +1671,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1678,7 +1687,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1693,7 +1703,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1708,7 +1719,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1723,7 +1735,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1738,7 +1751,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1753,7 +1767,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1768,7 +1783,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1783,7 +1799,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1798,7 +1815,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1813,7 +1831,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1829,7 +1848,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1850,7 +1870,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1858,6 +1879,24 @@
 )");
 }
 
+TEST_F(IR_BuilderImplTest, EmitStatement_Discard) {
+    auto* expr = Discard();
+    Func("test_function", {}, ty.void_(), expr,
+         utils::Vector{
+             create<ast::StageAttribute>(ast::PipelineStage::kFragment),
+         });
+
+    auto& b = CreateBuilder();
+    InjectFlowBlock();
+    b.EmitStatement(expr);
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+
+    Disassembler d(b.builder.ir);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
+    EXPECT_EQ(d.AsString(), R"(%1 (void) = discard
+)");
+}
+
 TEST_F(IR_BuilderImplTest, EmitStatement_UserFunction) {
     Func("my_func", utils::Vector{Param("p", ty.f32())}, ty.void_(), utils::Empty);
 
@@ -1866,8 +1905,8 @@
 
     auto& b = CreateBuilder();
     InjectFlowBlock();
-    auto r = b.EmitStatement(stmt);
-    ASSERT_TRUE(r) << b.error();
+    b.EmitStatement(stmt);
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1886,7 +1925,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1894,7 +1934,8 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Construct) {
+// Requires identifier expressions
+TEST_F(IR_BuilderImplTest, DISABLED_EmitExpression_Construct) {
     auto i = GlobalVar("i", builtin::AddressSpace::kPrivate, Expr(1_f));
     auto* expr = vec3(ty.f32(), 2_f, 3_f, i);
     WrapInFunction(expr);
@@ -1902,7 +1943,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1910,7 +1952,8 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Convert) {
+// Requires identifier expressions
+TEST_F(IR_BuilderImplTest, DISABLED_EmitExpression_Convert) {
     auto i = GlobalVar("i", builtin::AddressSpace::kPrivate, Expr(1_i));
     auto* expr = Call(ty.f32(), i);
     WrapInFunction(expr);
@@ -1918,7 +1961,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
@@ -1943,7 +1987,8 @@
 )");
 }
 
-TEST_F(IR_BuilderImplTest, EmitExpression_Builtin) {
+// Requires identifier expressions
+TEST_F(IR_BuilderImplTest, DISABLED_EmitExpression_Builtin) {
     auto i = GlobalVar("i", builtin::AddressSpace::kPrivate, Expr(1_f));
     auto* expr = Call("asin", i);
     WrapInFunction(expr);
@@ -1951,7 +1996,8 @@
     auto& b = CreateBuilder();
     InjectFlowBlock();
     auto r = b.EmitExpression(expr);
-    ASSERT_TRUE(r) << b.error();
+    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    ASSERT_TRUE(r);
 
     Disassembler d(b.builder.ir);
     d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
diff --git a/src/tint/ir/builtin.h b/src/tint/ir/builtin.h
index ee73ec6..b1fd61d 100644
--- a/src/tint/ir/builtin.h
+++ b/src/tint/ir/builtin.h
@@ -17,8 +17,6 @@
 
 #include "src/tint/builtin/function.h"
 #include "src/tint/ir/call.h"
-#include "src/tint/symbol_table.h"
-#include "src/tint/type/type.h"
 #include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
diff --git a/src/tint/ir/call.h b/src/tint/ir/call.h
index 89c07cd..e7bf684 100644
--- a/src/tint/ir/call.h
+++ b/src/tint/ir/call.h
@@ -16,8 +16,6 @@
 #define SRC_TINT_IR_CALL_H_
 
 #include "src/tint/ir/instruction.h"
-#include "src/tint/symbol_table.h"
-#include "src/tint/type/type.h"
 #include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
diff --git a/src/tint/ir/constant.h b/src/tint/ir/constant.h
index dd50fac..207af5d 100644
--- a/src/tint/ir/constant.h
+++ b/src/tint/ir/constant.h
@@ -17,7 +17,6 @@
 
 #include "src/tint/constant/value.h"
 #include "src/tint/ir/value.h"
-#include "src/tint/symbol_table.h"
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
diff --git a/src/tint/ir/construct.h b/src/tint/ir/construct.h
index f6c7a0a..c9d1819 100644
--- a/src/tint/ir/construct.h
+++ b/src/tint/ir/construct.h
@@ -16,8 +16,6 @@
 #define SRC_TINT_IR_CONSTRUCT_H_
 
 #include "src/tint/ir/call.h"
-#include "src/tint/symbol_table.h"
-#include "src/tint/type/type.h"
 #include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
diff --git a/src/tint/ir/convert.h b/src/tint/ir/convert.h
index 15d118e..d157e03 100644
--- a/src/tint/ir/convert.h
+++ b/src/tint/ir/convert.h
@@ -16,7 +16,6 @@
 #define SRC_TINT_IR_CONVERT_H_
 
 #include "src/tint/ir/call.h"
-#include "src/tint/symbol_table.h"
 #include "src/tint/type/type.h"
 #include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
diff --git a/src/tint/ir/temp.cc b/src/tint/ir/discard.cc
similarity index 62%
copy from src/tint/ir/temp.cc
copy to src/tint/ir/discard.cc
index b6c9a8e..1d179b5 100644
--- a/src/tint/ir/temp.cc
+++ b/src/tint/ir/discard.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Tint Authors.
+// Copyright 2023 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.
@@ -12,20 +12,20 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/ir/temp.h"
+#include "src/tint/ir/discard.h"
+#include "src/tint/debug.h"
 
-#include <string>
-
-TINT_INSTANTIATE_TYPEINFO(tint::ir::Temp);
+TINT_INSTANTIATE_TYPEINFO(tint::ir::Discard);
 
 namespace tint::ir {
 
-Temp::Temp(const type::Type* type, Id id) : type_(type), id_(id) {}
+Discard::Discard(Value* result) : Base(result) {}
 
-Temp::~Temp() = default;
+Discard::~Discard() = default;
 
-utils::StringStream& Temp::ToString(utils::StringStream& out) const {
-    out << "%" << std::to_string(AsId()) << " (" << type_->FriendlyName() << ")";
+utils::StringStream& Discard::ToString(utils::StringStream& out) const {
+    Result()->ToString(out);
+    out << " = discard";
     return out;
 }
 
diff --git a/src/tint/ir/discard.h b/src/tint/ir/discard.h
new file mode 100644
index 0000000..b27dc9d
--- /dev/null
+++ b/src/tint/ir/discard.h
@@ -0,0 +1,45 @@
+// Copyright 2023 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_IR_DISCARD_H_
+#define SRC_TINT_IR_DISCARD_H_
+
+#include "src/tint/ir/instruction.h"
+#include "src/tint/utils/castable.h"
+#include "src/tint/utils/string_stream.h"
+
+namespace tint::ir {
+
+/// A discard instruction in the IR.
+class Discard : public utils::Castable<Discard, Instruction> {
+  public:
+    /// Constructor
+    /// @param result the result id
+    explicit Discard(Value* result);
+    Discard(const Discard& instr) = delete;
+    Discard(Discard&& instr) = delete;
+    ~Discard() override;
+
+    Discard& operator=(const Discard& instr) = delete;
+    Discard& operator=(Discard&& instr) = delete;
+
+    /// Write the instruction to the given stream
+    /// @param out the stream to write to
+    /// @returns the stream
+    utils::StringStream& ToString(utils::StringStream& out) const override;
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_DISCARD_H_
diff --git a/src/tint/ir/discard_test.cc b/src/tint/ir/discard_test.cc
new file mode 100644
index 0000000..0f2fb5d
--- /dev/null
+++ b/src/tint/ir/discard_test.cc
@@ -0,0 +1,41 @@
+// Copyright 2023 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/ir/instruction.h"
+#include "src/tint/ir/test_helper.h"
+#include "src/tint/utils/string_stream.h"
+
+namespace tint::ir {
+namespace {
+
+using IR_InstructionTest = TestHelper;
+
+TEST_F(IR_InstructionTest, Discard) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_runtime_id = Runtime::Id(42);
+    const auto* instr = b.builder.Discard();
+
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
+    ASSERT_NE(instr->Result()->Type(), nullptr);
+    ASSERT_NE(instr->Result()->Type()->As<type::Void>(), nullptr);
+
+    utils::StringStream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 (void) = discard");
+}
+
+}  // namespace
+}  // namespace tint::ir
diff --git a/src/tint/ir/instruction.h b/src/tint/ir/instruction.h
index 8f58e54..df965e4 100644
--- a/src/tint/ir/instruction.h
+++ b/src/tint/ir/instruction.h
@@ -16,7 +16,6 @@
 #define SRC_TINT_IR_INSTRUCTION_H_
 
 #include "src/tint/ir/value.h"
-#include "src/tint/symbol_table.h"
 #include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
diff --git a/src/tint/ir/module.cc b/src/tint/ir/module.cc
index bf05e08..e434888 100644
--- a/src/tint/ir/module.cc
+++ b/src/tint/ir/module.cc
@@ -28,7 +28,7 @@
     BuilderImpl b(program);
     auto r = b.Build();
     if (!r) {
-        return b.error();
+        return b.Diagnostics().str();
     }
 
     return Result{r.Move()};
diff --git a/src/tint/ir/temp.cc b/src/tint/ir/runtime.cc
similarity index 74%
rename from src/tint/ir/temp.cc
rename to src/tint/ir/runtime.cc
index b6c9a8e..a1f485d 100644
--- a/src/tint/ir/temp.cc
+++ b/src/tint/ir/runtime.cc
@@ -12,19 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/ir/temp.h"
+#include "src/tint/ir/runtime.h"
 
 #include <string>
 
-TINT_INSTANTIATE_TYPEINFO(tint::ir::Temp);
+TINT_INSTANTIATE_TYPEINFO(tint::ir::Runtime);
 
 namespace tint::ir {
 
-Temp::Temp(const type::Type* type, Id id) : type_(type), id_(id) {}
+Runtime::Runtime(const type::Type* type, Id id) : type_(type), id_(id) {}
 
-Temp::~Temp() = default;
+Runtime::~Runtime() = default;
 
-utils::StringStream& Temp::ToString(utils::StringStream& out) const {
+utils::StringStream& Runtime::ToString(utils::StringStream& out) const {
     out << "%" << std::to_string(AsId()) << " (" << type_->FriendlyName() << ")";
     return out;
 }
diff --git a/src/tint/ir/temp.h b/src/tint/ir/runtime.h
similarity index 69%
rename from src/tint/ir/temp.h
rename to src/tint/ir/runtime.h
index 31bc284..6e2558c 100644
--- a/src/tint/ir/temp.h
+++ b/src/tint/ir/runtime.h
@@ -12,42 +12,41 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_IR_TEMP_H_
-#define SRC_TINT_IR_TEMP_H_
+#ifndef SRC_TINT_IR_RUNTIME_H_
+#define SRC_TINT_IR_RUNTIME_H_
 
 #include "src/tint/ir/value.h"
-#include "src/tint/symbol_table.h"
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
-/// Temporary value in the IR.
-class Temp : public utils::Castable<Temp, Value> {
+/// Runtime value in the IR.
+class Runtime : public utils::Castable<Runtime, Value> {
   public:
     /// A value id.
     using Id = uint32_t;
 
     /// Constructor
-    /// @param type the type of the temporary
+    /// @param type the type of the value
     /// @param id the id for the value
-    Temp(const type::Type* type, Id id);
+    Runtime(const type::Type* type, Id id);
 
     /// Destructor
-    ~Temp() override;
+    ~Runtime() override;
 
-    Temp(const Temp&) = delete;
-    Temp(Temp&&) = delete;
+    Runtime(const Runtime&) = delete;
+    Runtime(Runtime&&) = delete;
 
-    Temp& operator=(const Temp&) = delete;
-    Temp& operator=(Temp&&) = delete;
+    Runtime& operator=(const Runtime&) = delete;
+    Runtime& operator=(Runtime&&) = delete;
 
     /// @returns the value data as an `Id`.
     Id AsId() const { return id_; }
 
-    /// @returns the type of the temporary
+    /// @returns the type of the value
     const type::Type* Type() const override { return type_; }
 
-    /// Write the temp to the given stream
+    /// Write the id to the given stream
     /// @param out the stream to write to
     /// @returns the stream
     utils::StringStream& ToString(utils::StringStream& out) const override;
@@ -59,4 +58,4 @@
 
 }  // namespace tint::ir
 
-#endif  // SRC_TINT_IR_TEMP_H_
+#endif  // SRC_TINT_IR_RUNTIME_H_
diff --git a/src/tint/ir/temp_test.cc b/src/tint/ir/runtime_test.cc
similarity index 81%
rename from src/tint/ir/temp_test.cc
rename to src/tint/ir/runtime_test.cc
index edd002b..7959909 100644
--- a/src/tint/ir/temp_test.cc
+++ b/src/tint/ir/runtime_test.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/ir/temp.h"
+#include "src/tint/ir/runtime.h"
 #include "src/tint/ir/test_helper.h"
 #include "src/tint/utils/string_stream.h"
 
@@ -21,15 +21,15 @@
 
 using namespace tint::number_suffixes;  // NOLINT
 
-using IR_TempTest = TestHelper;
+using IR_RuntimeTest = TestHelper;
 
-TEST_F(IR_TempTest, id) {
+TEST_F(IR_RuntimeTest, id) {
     auto& b = CreateEmptyBuilder();
 
     utils::StringStream str;
 
-    b.builder.next_temp_id = Temp::Id(4);
-    auto* val = b.builder.Temp(b.builder.ir.types.Get<type::I32>());
+    b.builder.next_runtime_id = Runtime::Id(4);
+    auto* val = b.builder.Runtime(b.builder.ir.types.Get<type::I32>());
     EXPECT_EQ(4u, val->AsId());
 
     val->ToString(str);
diff --git a/src/tint/ir/store.cc b/src/tint/ir/store.cc
new file mode 100644
index 0000000..32162c3
--- /dev/null
+++ b/src/tint/ir/store.cc
@@ -0,0 +1,36 @@
+// Copyright 2023 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/ir/store.h"
+#include "src/tint/debug.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ir::Store);
+
+namespace tint::ir {
+
+Store::Store(Value* to, Value* from) : Base(to), from_(from) {
+    TINT_ASSERT(IR, from_);
+    from_->AddUsage(this);
+}
+
+Store::~Store() = default;
+
+utils::StringStream& Store::ToString(utils::StringStream& out) const {
+    Result()->ToString(out);
+    out << " = ";
+    from_->ToString(out);
+    return out;
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/store.h b/src/tint/ir/store.h
new file mode 100644
index 0000000..57544fc
--- /dev/null
+++ b/src/tint/ir/store.h
@@ -0,0 +1,52 @@
+// Copyright 2023 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_IR_STORE_H_
+#define SRC_TINT_IR_STORE_H_
+
+#include "src/tint/ir/instruction.h"
+#include "src/tint/utils/castable.h"
+#include "src/tint/utils/string_stream.h"
+
+namespace tint::ir {
+
+/// An instruction in the IR.
+class Store : public utils::Castable<Store, Instruction> {
+  public:
+    /// Constructor
+    /// @param to the value to store too
+    /// @param from the value being stored from
+    Store(Value* to, Value* from);
+    Store(const Store& instr) = delete;
+    Store(Store&& instr) = delete;
+    ~Store() override;
+
+    Store& operator=(const Store& instr) = delete;
+    Store& operator=(Store&& instr) = delete;
+
+    /// @returns the value being stored
+    const Value* from() const { return from_; }
+
+    /// Write the instruction to the given stream
+    /// @param out the stream to write to
+    /// @returns the stream
+    utils::StringStream& ToString(utils::StringStream& out) const override;
+
+  private:
+    Value* from_ = nullptr;
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_STORE_H_
diff --git a/src/tint/ir/store_test.cc b/src/tint/ir/store_test.cc
new file mode 100644
index 0000000..e0632dd
--- /dev/null
+++ b/src/tint/ir/store_test.cc
@@ -0,0 +1,65 @@
+// Copyright 2023 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/ir/instruction.h"
+#include "src/tint/ir/test_helper.h"
+#include "src/tint/utils/string_stream.h"
+
+namespace tint::ir {
+namespace {
+
+using namespace tint::number_suffixes;  // NOLINT
+
+using IR_InstructionTest = TestHelper;
+
+TEST_F(IR_InstructionTest, CreateStore) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_runtime_id = Runtime::Id(42);
+
+    auto* rt = b.builder.Runtime(b.builder.ir.types.Get<type::I32>());
+    const auto* instr = b.builder.Store(rt, b.builder.Constant(4_i));
+
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    ASSERT_NE(instr->Result()->Type(), nullptr);
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
+
+    ASSERT_TRUE(instr->from()->Is<Constant>());
+    auto lhs = instr->from()->As<Constant>()->value;
+    ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
+    EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
+
+    utils::StringStream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 (i32) = 4");
+}
+
+TEST_F(IR_InstructionTest, Store_Usage) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_runtime_id = Runtime::Id(42);
+    auto* rt = b.builder.Runtime(b.builder.ir.types.Get<type::I32>());
+    const auto* instr = b.builder.Store(rt, b.builder.Constant(4_i));
+
+    ASSERT_NE(instr->Result(), nullptr);
+    ASSERT_EQ(instr->Result()->Usage().Length(), 1u);
+    EXPECT_EQ(instr->Result()->Usage()[0], instr);
+
+    ASSERT_NE(instr->from(), nullptr);
+    ASSERT_EQ(instr->from()->Usage().Length(), 1u);
+    EXPECT_EQ(instr->from()->Usage()[0], instr);
+}
+
+}  // namespace
+}  // namespace tint::ir
diff --git a/src/tint/ir/test_helper.h b/src/tint/ir/test_helper.h
index 6e36ed3..47f8821 100644
--- a/src/tint/ir/test_helper.h
+++ b/src/tint/ir/test_helper.h
@@ -79,7 +79,7 @@
         auto m = b.Build();
 
         // Store the error away in case we need it
-        error_ = b.error();
+        error_ = b.Diagnostics().str();
 
         // Explicitly remove program to guard against pointers back to ast. Note, this does mean the
         // BuilderImpl is pointing to an invalid program. We keep the BuilderImpl around because we
diff --git a/src/tint/ir/unary.cc b/src/tint/ir/unary.cc
new file mode 100644
index 0000000..532efcc
--- /dev/null
+++ b/src/tint/ir/unary.cc
@@ -0,0 +1,53 @@
+// Copyright 2023 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/ir/unary.h"
+#include "src/tint/debug.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ir::Unary);
+
+namespace tint::ir {
+
+Unary::Unary(Kind kind, Value* result, Value* val) : Base(result), kind_(kind), val_(val) {
+    TINT_ASSERT(IR, val_);
+    val_->AddUsage(this);
+}
+
+Unary::~Unary() = default;
+
+utils::StringStream& Unary::ToString(utils::StringStream& out) const {
+    Result()->ToString(out) << " = ";
+    switch (GetKind()) {
+        case Unary::Kind::kAddressOf:
+            out << "&";
+            break;
+        case Unary::Kind::kComplement:
+            out << "~";
+            break;
+        case Unary::Kind::kIndirection:
+            out << "*";
+            break;
+        case Unary::Kind::kNegation:
+            out << "-";
+            break;
+        case Unary::Kind::kNot:
+            out << "!";
+            break;
+    }
+    val_->ToString(out);
+
+    return out;
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/unary.h b/src/tint/ir/unary.h
new file mode 100644
index 0000000..0337b4f
--- /dev/null
+++ b/src/tint/ir/unary.h
@@ -0,0 +1,66 @@
+// Copyright 2023 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_IR_UNARY_H_
+#define SRC_TINT_IR_UNARY_H_
+
+#include "src/tint/ir/instruction.h"
+#include "src/tint/utils/castable.h"
+#include "src/tint/utils/string_stream.h"
+
+namespace tint::ir {
+
+/// An instruction in the IR.
+class Unary : public utils::Castable<Unary, Instruction> {
+  public:
+    /// The kind of instruction.
+    enum class Kind {
+        kAddressOf,
+        kComplement,
+        kIndirection,
+        kNegation,
+        kNot,
+    };
+
+    /// Constructor
+    /// @param kind the kind of unary instruction
+    /// @param result the result value
+    /// @param val the lhs of the instruction
+    Unary(Kind kind, Value* result, Value* val);
+    Unary(const Unary& instr) = delete;
+    Unary(Unary&& instr) = delete;
+    ~Unary() override;
+
+    Unary& operator=(const Unary& instr) = delete;
+    Unary& operator=(Unary&& instr) = delete;
+
+    /// @returns the kind of instruction
+    Kind GetKind() const { return kind_; }
+
+    /// @returns the value for the instruction
+    const Value* Val() const { return val_; }
+
+    /// Write the instruction to the given stream
+    /// @param out the stream to write to
+    /// @returns the stream
+    utils::StringStream& ToString(utils::StringStream& out) const override;
+
+  private:
+    Kind kind_;
+    Value* val_ = nullptr;
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_UNARY_H_
diff --git a/src/tint/ir/unary_test.cc b/src/tint/ir/unary_test.cc
new file mode 100644
index 0000000..cfc5579
--- /dev/null
+++ b/src/tint/ir/unary_test.cc
@@ -0,0 +1,161 @@
+// Copyright 2023 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/ir/instruction.h"
+#include "src/tint/ir/test_helper.h"
+#include "src/tint/utils/string_stream.h"
+
+namespace tint::ir {
+namespace {
+
+using namespace tint::number_suffixes;  // NOLINT
+
+using IR_InstructionTest = TestHelper;
+
+TEST_F(IR_InstructionTest, CreateAddressOf) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_runtime_id = Runtime::Id(42);
+    // TODO(dsinclair): This would be better as an identifier, but works for now.
+    const auto* instr =
+        b.builder.AddressOf(b.builder.ir.types.Get<type::Pointer>(
+                                b.builder.ir.types.Get<type::I32>(),
+                                builtin::AddressSpace::kPrivate, builtin::Access::kReadWrite),
+                            b.builder.Constant(4_i));
+
+    EXPECT_EQ(instr->GetKind(), Unary::Kind::kAddressOf);
+
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    ASSERT_NE(instr->Result()->Type(), nullptr);
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
+
+    ASSERT_TRUE(instr->Val()->Is<Constant>());
+    auto lhs = instr->Val()->As<Constant>()->value;
+    ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
+    EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
+
+    utils::StringStream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 (ptr<private, i32, read_write>) = &4");
+}
+
+TEST_F(IR_InstructionTest, CreateComplement) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_runtime_id = Runtime::Id(42);
+    const auto* instr =
+        b.builder.Complement(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
+
+    EXPECT_EQ(instr->GetKind(), Unary::Kind::kComplement);
+
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
+
+    ASSERT_TRUE(instr->Val()->Is<Constant>());
+    auto lhs = instr->Val()->As<Constant>()->value;
+    ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
+    EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
+
+    utils::StringStream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 (i32) = ~4");
+}
+
+TEST_F(IR_InstructionTest, CreateIndirection) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_runtime_id = Runtime::Id(42);
+    // TODO(dsinclair): This would be better as an identifier, but works for now.
+    const auto* instr =
+        b.builder.Indirection(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
+
+    EXPECT_EQ(instr->GetKind(), Unary::Kind::kIndirection);
+
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
+
+    ASSERT_TRUE(instr->Val()->Is<Constant>());
+    auto lhs = instr->Val()->As<Constant>()->value;
+    ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
+    EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
+
+    utils::StringStream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 (i32) = *4");
+}
+
+TEST_F(IR_InstructionTest, CreateNegation) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_runtime_id = Runtime::Id(42);
+    const auto* instr =
+        b.builder.Negation(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
+
+    EXPECT_EQ(instr->GetKind(), Unary::Kind::kNegation);
+
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
+
+    ASSERT_TRUE(instr->Val()->Is<Constant>());
+    auto lhs = instr->Val()->As<Constant>()->value;
+    ASSERT_TRUE(lhs->Is<constant::Scalar<i32>>());
+    EXPECT_EQ(4_i, lhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
+
+    utils::StringStream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 (i32) = -4");
+}
+
+TEST_F(IR_InstructionTest, CreateNot) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_runtime_id = Runtime::Id(42);
+    const auto* instr =
+        b.builder.Not(b.builder.ir.types.Get<type::Bool>(), b.builder.Constant(true));
+
+    EXPECT_EQ(instr->GetKind(), Unary::Kind::kNot);
+
+    ASSERT_TRUE(instr->Result()->Is<Runtime>());
+    EXPECT_EQ(Runtime::Id(42), instr->Result()->As<Runtime>()->AsId());
+
+    ASSERT_TRUE(instr->Val()->Is<Constant>());
+    auto lhs = instr->Val()->As<Constant>()->value;
+    ASSERT_TRUE(lhs->Is<constant::Scalar<bool>>());
+    EXPECT_TRUE(lhs->As<constant::Scalar<bool>>()->ValueAs<bool>());
+
+    utils::StringStream str;
+    instr->ToString(str);
+    EXPECT_EQ(str.str(), "%42 (bool) = !true");
+}
+
+TEST_F(IR_InstructionTest, Unary_Usage) {
+    auto& b = CreateEmptyBuilder();
+
+    b.builder.next_runtime_id = Runtime::Id(42);
+    const auto* instr =
+        b.builder.Negation(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
+
+    EXPECT_EQ(instr->GetKind(), Unary::Kind::kNegation);
+
+    ASSERT_NE(instr->Result(), nullptr);
+    ASSERT_EQ(instr->Result()->Usage().Length(), 1u);
+    EXPECT_EQ(instr->Result()->Usage()[0], instr);
+
+    ASSERT_NE(instr->Val(), nullptr);
+    ASSERT_EQ(instr->Val()->Usage().Length(), 1u);
+    EXPECT_EQ(instr->Val()->Usage()[0], instr);
+}
+
+}  // namespace
+}  // namespace tint::ir
diff --git a/src/tint/ir/user_call.h b/src/tint/ir/user_call.h
index 8c99d89..0662455 100644
--- a/src/tint/ir/user_call.h
+++ b/src/tint/ir/user_call.h
@@ -16,8 +16,7 @@
 #define SRC_TINT_IR_USER_CALL_H_
 
 #include "src/tint/ir/call.h"
-#include "src/tint/symbol_table.h"
-#include "src/tint/type/type.h"
+#include "src/tint/symbol.h"
 #include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
 
diff --git a/src/tint/ir/value.cc b/src/tint/ir/value.cc
index 93040cf..750fe19 100644
--- a/src/tint/ir/value.cc
+++ b/src/tint/ir/value.cc
@@ -15,7 +15,7 @@
 #include "src/tint/ir/value.h"
 
 #include "src/tint/ir/constant.h"
-#include "src/tint/ir/temp.h"
+#include "src/tint/ir/runtime.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ir::Value);
 
diff --git a/src/tint/ir/value.h b/src/tint/ir/value.h
index b498e9d..e2a642b 100644
--- a/src/tint/ir/value.h
+++ b/src/tint/ir/value.h
@@ -15,7 +15,6 @@
 #ifndef SRC_TINT_IR_VALUE_H_
 #define SRC_TINT_IR_VALUE_H_
 
-#include "src/tint/symbol_table.h"
 #include "src/tint/type/type.h"
 #include "src/tint/utils/castable.h"
 #include "src/tint/utils/string_stream.h"
diff --git a/src/tint/reader/spirv/function_arithmetic_test.cc b/src/tint/reader/spirv/function_arithmetic_test.cc
index 8f29a91..1f14633 100644
--- a/src/tint/reader/spirv/function_arithmetic_test.cc
+++ b/src/tint/reader/spirv/function_arithmetic_test.cc
@@ -30,6 +30,11 @@
   OpEntryPoint Fragment %100 "main"
   OpExecutionMode %100 OriginUpperLeft
 
+  OpName %v2float_50_60 "v2float_50_60"
+  OpName %v2float_60_50 "v2float_60_50"
+  OpName %v3float_50_60_70 "v3float_50_60_70"
+  OpName %v3float_60_70_50 "v3float_60_70_50"
+
   %void = OpTypeVoid
   %voidfn = OpTypeFunction %void
 
@@ -94,10 +99,10 @@
         return "bitcast<vec2<u32>>(vec2<i32>(40i, 30i))";
     }
     if (assembly == "v2float_50_60") {
-        return "vec2<f32>(50.0f, 60.0f)";
+        return "v2float_50_60";
     }
     if (assembly == "v2float_60_50") {
-        return "vec2<f32>(60.0f, 50.0f)";
+        return "v2float_60_50";
     }
     return "bad case";
 }
@@ -271,7 +276,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2<f32> = -(vec2<f32>(50.0f, 60.0f));"));
+                HasSubstr("let x_1 : vec2<f32> = -(v2float_50_60);"));
 }
 
 struct BinaryData {
@@ -704,10 +709,9 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(
-        test::ToString(p->program(), ast_body),
-        HasSubstr(
-            R"(let x_1 : vec2<f32> = (vec2<f32>(50.0f, 60.0f) - (vec2<f32>(60.0f, 50.0f) * floor((vec2<f32>(50.0f, 60.0f) / vec2<f32>(60.0f, 50.0f)))));)"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body),
+                HasSubstr("let x_1 : vec2<f32> = (v2float_50_60 - (v2float_60_50 * "
+                          "floor((v2float_50_60 / v2float_60_50))));"));
 }
 
 TEST_F(SpvBinaryArithTestBasic, VectorTimesScalar) {
diff --git a/src/tint/reader/spirv/function_bit_test.cc b/src/tint/reader/spirv/function_bit_test.cc
index 2a12f01..0401a63 100644
--- a/src/tint/reader/spirv/function_bit_test.cc
+++ b/src/tint/reader/spirv/function_bit_test.cc
@@ -877,8 +877,7 @@
     auto body = test::ToString(p->program(), ast_body);
     EXPECT_THAT(
         body,
-        HasSubstr(
-            R"(let x_1 : vec2<i32> = insertBits(vec2<i32>(30i, 40i), vec2<i32>(40i, 30i), 10u, 20u);)"))
+        HasSubstr(R"(let x_1 : vec2<i32> = insertBits(x_28, vec2<i32>(40i, 30i), 10u, 20u);)"))
         << body;
 }
 
@@ -897,7 +896,7 @@
     EXPECT_THAT(
         body,
         HasSubstr(
-            R"(let x_1 : vec2<i32> = insertBits(vec2<i32>(30i, 40i), vec2<i32>(40i, 30i), u32(10i), u32(20i));)"))
+            R"(let x_1 : vec2<i32> = insertBits(x_28, vec2<i32>(40i, 30i), u32(10i), u32(20i));)"))
         << body;
 }
 
@@ -946,8 +945,7 @@
     auto body = test::ToString(p->program(), ast_body);
     EXPECT_THAT(
         body,
-        HasSubstr(
-            R"(let x_1 : vec2<u32> = insertBits(vec2<u32>(10u, 20u), vec2<u32>(20u, 10u), 10u, 20u);)"))
+        HasSubstr(R"(let x_1 : vec2<u32> = insertBits(x_26, vec2<u32>(20u, 10u), 10u, 20u);)"))
         << body;
 }
 
@@ -966,7 +964,7 @@
     EXPECT_THAT(
         body,
         HasSubstr(
-            R"(let x_1 : vec2<u32> = insertBits(vec2<u32>(10u, 20u), vec2<u32>(20u, 10u), u32(10i), u32(20i));)"))
+            R"(let x_1 : vec2<u32> = insertBits(x_26, vec2<u32>(20u, 10u), u32(10i), u32(20i));)"))
         << body;
 }
 
@@ -1012,9 +1010,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body,
-                HasSubstr("let x_1 : vec2<i32> = extractBits(vec2<i32>(30i, 40i), 10u, 20u);"))
-        << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 : vec2<i32> = extractBits(x_28, 10u, 20u);")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, ExtractBits_IntVector_SignedOffsetAndCount) {
@@ -1029,9 +1025,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(
-        body,
-        HasSubstr("let x_1 : vec2<i32> = extractBits(vec2<i32>(30i, 40i), u32(10i), u32(20i));"))
+    EXPECT_THAT(body, HasSubstr("let x_1 : vec2<i32> = extractBits(x_28, u32(10i), u32(20i));"))
         << body;
 }
 
@@ -1077,9 +1071,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body,
-                HasSubstr("let x_1 : vec2<u32> = extractBits(vec2<u32>(10u, 20u), 10u, 20u);"))
-        << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 : vec2<u32> = extractBits(x_26, 10u, 20u);")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, ExtractBits_UintVector_SignedOffsetAndCount) {
@@ -1094,9 +1086,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(
-        body,
-        HasSubstr("let x_1 : vec2<u32> = extractBits(vec2<u32>(10u, 20u), u32(10i), u32(20i));"))
+    EXPECT_THAT(body, HasSubstr("let x_1 : vec2<u32> = extractBits(x_26, u32(10i), u32(20i));"))
         << body;
 }
 
diff --git a/src/tint/reader/spirv/parser_impl.cc b/src/tint/reader/spirv/parser_impl.cc
index 9f5afdb..6f6bf1e 100644
--- a/src/tint/reader/spirv/parser_impl.cc
+++ b/src/tint/reader/spirv/parser_impl.cc
@@ -1438,6 +1438,12 @@
         if ((type_id == builtin_position_.pointer_type_id) &&
             ((spirv_storage_class == spv::StorageClass::Input) ||
              (spirv_storage_class == spv::StorageClass::Output))) {
+            // TODO(crbug.com/tint/103): Support modules that contain multiple Position built-ins.
+            if (builtin_position_.per_vertex_var_id != 0) {
+                return Fail()
+                       << "unsupported: multiple Position built-in variables in the same module";
+            }
+
             // Skip emitting gl_PerVertex.
             builtin_position_.per_vertex_var_id = var.result_id();
             builtin_position_.per_vertex_var_init_id =
@@ -1910,6 +1916,13 @@
         case spv::Op::OpConstantComposite: {
             // Handle vector, matrix, array, and struct
 
+            auto itr = declared_constant_composites_.find(id);
+            if (itr != declared_constant_composites_.end()) {
+                // We've already declared this constant value as a module-scope const, so just
+                // reference that identifier.
+                return {original_ast_type, builder_.Expr(itr->second)};
+            }
+
             // Generate a composite from explicit components.
             ExpressionList ast_components;
             if (!inst->WhileEachInId([&](const uint32_t* id_ref) -> bool {
@@ -1924,8 +1937,20 @@
                 // We've already emitted a diagnostic.
                 return {};
             }
-            return {original_ast_type, builder_.Call(source, original_ast_type->Build(builder_),
-                                                     std::move(ast_components))};
+
+            auto* expr = builder_.Call(source, original_ast_type->Build(builder_),
+                                       std::move(ast_components));
+
+            if (def_use_mgr_->NumUses(id) == 1) {
+                // The constant is only used once, so just inline its use.
+                return {original_ast_type, expr};
+            }
+
+            // Create a module-scope const declaration for the constant.
+            auto name = namer_.Name(id);
+            auto* decl = builder_.GlobalConst(name, expr);
+            declared_constant_composites_.insert({id, decl->name->symbol});
+            return {original_ast_type, builder_.Expr(name)};
         }
         default:
             break;
diff --git a/src/tint/reader/spirv/parser_impl.h b/src/tint/reader/spirv/parser_impl.h
index 141df61..e0ef655 100644
--- a/src/tint/reader/spirv/parser_impl.h
+++ b/src/tint/reader/spirv/parser_impl.h
@@ -880,6 +880,9 @@
     // The ast::Struct type names with only read-only members.
     std::unordered_set<Symbol> read_only_struct_types_;
 
+    // Maps from OpConstantComposite IDs to identifiers of module-scope const declarations.
+    std::unordered_map<uint32_t, Symbol> declared_constant_composites_;
+
     // The IDs of scalar spec constants
     std::unordered_set<uint32_t> scalar_spec_constants_;
 
diff --git a/src/tint/reader/spirv/parser_impl_handle_test.cc b/src/tint/reader/spirv/parser_impl_handle_test.cc
index 39d5498..dd572b6 100644
--- a/src/tint/reader/spirv/parser_impl_handle_test.cc
+++ b/src/tint/reader/spirv/parser_impl_handle_test.cc
@@ -1504,7 +1504,7 @@
                         R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-                        "textureGather(1i, x_20, x_10, coords12, vec2<i32>(3i, 4i))"},
+                        "textureGather(1i, x_20, x_10, coords12, offsets2d)"},
         // OpImageGather 2D ConstOffset unsigned
         ImageAccessCase{"%float 2D 0 0 0 1 Unknown",
                         "%result = OpImageGather "
@@ -1514,7 +1514,7 @@
 
 @group(2) @binding(1) var x_20 : texture_2d<f32>;)",
                         "textureGather(1i, x_20, x_10, coords12, "
-                        "vec2<i32>(vec2<u32>(3u, 4u)))"},
+                        "vec2<i32>(u_offsets2d))"},
         // OpImageGather 2D Array
         ImageAccessCase{"%float 2D 0 1 0 1 Unknown",
                         "%result = OpImageGather "
@@ -1532,7 +1532,7 @@
 
 @group(2) @binding(1) var x_20 : texture_2d_array<f32>;)",
                         "textureGather(1i, x_20, x_10, coords123.xy, "
-                        "i32(round(coords123.z)), vec2<i32>(3i, 4i))"},
+                        "i32(round(coords123.z)), offsets2d)"},
         // OpImageGather 2D Array ConstOffset unsigned
         ImageAccessCase{"%float 2D 0 1 0 1 Unknown",
                         "%result = OpImageGather "
@@ -1543,7 +1543,7 @@
 @group(2) @binding(1) var x_20 : texture_2d_array<f32>;)",
                         "textureGather(1i, x_20, x_10, coords123.xy, "
                         "i32(round(coords123.z)), "
-                        "vec2<i32>(vec2<u32>(3u, 4u)))"},
+                        "vec2<i32>(u_offsets2d))"},
         // OpImageGather Cube
         ImageAccessCase{"%float Cube 0 0 0 1 Unknown",
                         "%result = OpImageGather "
@@ -1576,7 +1576,7 @@
                         R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_depth_2d;)",
-                        "textureGather(x_20, x_10, coords12, vec2<i32>(3i, 4i))"},
+                        "textureGather(x_20, x_10, coords12, offsets2d)"},
         // OpImageGather 2DDepth ConstOffset unsigned
         ImageAccessCase{"%float 2D 1 0 0 1 Unknown",
                         "%result = OpImageGather "
@@ -1586,7 +1586,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_2d;)",
                         "textureGather(x_20, x_10, coords12, "
-                        "vec2<i32>(vec2<u32>(3u, 4u)))"},
+                        "vec2<i32>(u_offsets2d))"},
         // OpImageGather 2DDepth Array
         ImageAccessCase{"%float 2D 1 1 0 1 Unknown",
                         "%result = OpImageGather "
@@ -1604,7 +1604,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_2d_array;)",
                         "textureGather(x_20, x_10, coords123.xy, "
-                        "i32(round(coords123.z)), vec2<i32>(3i, 4i))"},
+                        "i32(round(coords123.z)), offsets2d)"},
         // OpImageGather 2DDepth Array ConstOffset unsigned
         ImageAccessCase{"%float 2D 1 1 0 1 Unknown",
                         "%result = OpImageGather "
@@ -1615,7 +1615,7 @@
 @group(2) @binding(1) var x_20 : texture_depth_2d_array;)",
                         "textureGather(x_20, x_10, coords123.xy, "
                         "i32(round(coords123.z)), "
-                        "vec2<i32>(vec2<u32>(3u, 4u)))"},
+                        "vec2<i32>(u_offsets2d))"},
         // OpImageGather DepthCube
         ImageAccessCase{"%float Cube 1 0 0 1 Unknown",
                         "%result = OpImageGather "
@@ -1654,7 +1654,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_2d;)",
                         "textureGatherCompare(x_20, x_10, coords12, 0.20000000298023223877f, "
-                        "vec2<i32>(3i, 4i))"},
+                        "offsets2d)"},
         // OpImageDrefGather 2DDepth ConstOffset unsigned
         ImageAccessCase{"%float 2D 1 0 0 1 Unknown",
                         "%result = OpImageDrefGather "
@@ -1664,7 +1664,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_2d;)",
                         "textureGatherCompare(x_20, x_10, coords12, 0.20000000298023223877f, "
-                        "vec2<i32>(vec2<u32>(3u, 4u)))"},
+                        "vec2<i32>(u_offsets2d))"},
         // OpImageDrefGather 2DDepth Array
         ImageAccessCase{"%float 2D 1 1 0 1 Unknown",
                         "%result = OpImageDrefGather "
@@ -1682,7 +1682,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_2d_array;)",
                         "textureGatherCompare(x_20, x_10, coords123.xy, "
-                        "i32(round(coords123.z)), 0.20000000298023223877f, vec2<i32>(3i, 4i))"},
+                        "i32(round(coords123.z)), 0.20000000298023223877f, offsets2d)"},
         // OpImageDrefGather 2DDepth Array ConstOffset unsigned
         ImageAccessCase{"%float 2D 1 1 0 1 Unknown",
                         "%result = OpImageDrefGather "
@@ -1693,7 +1693,7 @@
 @group(2) @binding(1) var x_20 : texture_depth_2d_array;)",
                         "textureGatherCompare(x_20, x_10, coords123.xy, "
                         "i32(round(coords123.z)), 0.20000000298023223877f, "
-                        "vec2<i32>(vec2<u32>(3u, 4u)))"},
+                        "vec2<i32>(u_offsets2d))"},
         // OpImageDrefGather DepthCube
         ImageAccessCase{"%float Cube 1 0 0 1 Unknown",
                         "%result = OpImageDrefGather "
@@ -1742,7 +1742,7 @@
                         R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-                        "textureSample(x_20, x_10, coords12, vec2<i32>(3i, 4i))"},
+                        "textureSample(x_20, x_10, coords12, offsets2d)"},
 
         // OpImageSampleImplicitLod arrayed with ConstOffset
         ImageAccessCase{
@@ -1752,7 +1752,7 @@
             R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d_array<f32>;)",
-            R"(textureSample(x_20, x_10, coords123.xy, i32(round(coords123.z)), vec2<i32>(3i, 4i)))"},
+            R"(textureSample(x_20, x_10, coords123.xy, i32(round(coords123.z)), offsets2d))"},
 
         // OpImageSampleImplicitLod with Bias
         ImageAccessCase{"%float 2D 0 0 0 1 Unknown",
@@ -1781,19 +1781,18 @@
                         R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-                        R"(textureSampleBias(x_20, x_10, coords12, 7.0f, vec2<i32>(3i, 4i))"},
+                        R"(textureSampleBias(x_20, x_10, coords12, 7.0f, offsets2d)"},
 
         // OpImageSampleImplicitLod with Bias and unsigned ConstOffset
         // Convert ConstOffset to signed
-        ImageAccessCase{
-            "%float 2D 0 0 0 1 Unknown",
-            "%result = OpImageSampleImplicitLod "
-            "%v4float %sampled_image %coords12 Bias|ConstOffset "
-            "%float_7 %u_offsets2d",
-            R"(@group(0) @binding(0) var x_10 : sampler;
+        ImageAccessCase{"%float 2D 0 0 0 1 Unknown",
+                        "%result = OpImageSampleImplicitLod "
+                        "%v4float %sampled_image %coords12 Bias|ConstOffset "
+                        "%float_7 %u_offsets2d",
+                        R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-            R"(textureSampleBias(x_20, x_10, coords12, 7.0f, vec2<i32>(vec2<u32>(3u, 4u)))"},
+                        R"(textureSampleBias(x_20, x_10, coords12, 7.0f, vec2<i32>(u_offsets2d))"},
         // OpImageSampleImplicitLod arrayed with Bias
         ImageAccessCase{
             "%float 2D 0 1 0 1 Unknown",
@@ -1803,7 +1802,7 @@
             R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d_array<f32>;)",
-            R"(textureSampleBias(x_20, x_10, coords123.xy, i32(round(coords123.z)), 7.0f, vec2<i32>(3i, 4i))"}));
+            R"(textureSampleBias(x_20, x_10, coords123.xy, i32(round(coords123.z)), 7.0f, offsets2d)"}));
 
 INSTANTIATE_TEST_SUITE_P(
     // This test shows the use of a sampled image used with both regular
@@ -1863,7 +1862,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_2d;
 )",
-            R"(textureSampleCompare(x_20, x_10, coords12, 0.20000000298023223877f, vec2<i32>(3i, 4i)))"},
+            R"(textureSampleCompare(x_20, x_10, coords12, 0.20000000298023223877f, offsets2d))"},
         // ImageSampleDrefImplicitLod arrayed with ConstOffset
         ImageAccessCase{
             "%float 2D 0 1 0 1 Unknown",
@@ -1872,7 +1871,7 @@
             R"(@group(0) @binding(0) var x_10 : sampler_comparison;
 
 @group(2) @binding(1) var x_20 : texture_depth_2d_array;)",
-            R"(textureSampleCompare(x_20, x_10, coords123.xy, i32(round(coords123.z)), 0.20000000298023223877f, vec2<i32>(3i, 4i)))"}));
+            R"(textureSampleCompare(x_20, x_10, coords123.xy, i32(round(coords123.z)), 0.20000000298023223877f, offsets2d))"}));
 
 INSTANTIATE_TEST_SUITE_P(
     ImageSampleDrefExplicitLod,
@@ -1909,7 +1908,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_2d;
 )",
-            R"(textureSampleCompareLevel(x_20, x_10, coords12, 0.20000000298023223877f, vec2<i32>(3i, 4i)))"},
+            R"(textureSampleCompareLevel(x_20, x_10, coords12, 0.20000000298023223877f, offsets2d))"},
         // 2D array, ConstOffset
         ImageAccessCase{
             "%float 2D 1 1 0 1 Unknown",
@@ -1919,7 +1918,7 @@
             R"(@group(0) @binding(0) var x_10 : sampler_comparison;
 
 @group(2) @binding(1) var x_20 : texture_depth_2d_array;)",
-            R"(textureSampleCompareLevel(x_20, x_10, coords123.xy, i32(round(coords123.z)), 0.20000000298023223877f, vec2<i32>(3i, 4i)))"},
+            R"(textureSampleCompareLevel(x_20, x_10, coords123.xy, i32(round(coords123.z)), 0.20000000298023223877f, offsets2d))"},
         // Cube
         ImageAccessCase{
             "%float Cube 1 0 0 1 Unknown",
@@ -1971,19 +1970,18 @@
                         R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-                        R"(textureSampleLevel(x_20, x_10, coords12, 0.0f, vec2<i32>(3i, 4i)))"},
+                        R"(textureSampleLevel(x_20, x_10, coords12, 0.0f, offsets2d))"},
 
         // OpImageSampleExplicitLod - using Lod and unsigned ConstOffset
         // Convert the ConstOffset operand to signed
-        ImageAccessCase{
-            "%float 2D 0 0 0 1 Unknown",
-            "%result = OpImageSampleExplicitLod "
-            "%v4float %sampled_image %coords12 Lod|ConstOffset "
-            "%float_null %u_offsets2d",
-            R"(@group(0) @binding(0) var x_10 : sampler;
+        ImageAccessCase{"%float 2D 0 0 0 1 Unknown",
+                        "%result = OpImageSampleExplicitLod "
+                        "%v4float %sampled_image %coords12 Lod|ConstOffset "
+                        "%float_null %u_offsets2d",
+                        R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-            R"(textureSampleLevel(x_20, x_10, coords12, 0.0f, vec2<i32>(vec2<u32>(3u, 4u)))"},
+                        R"(textureSampleLevel(x_20, x_10, coords12, 0.0f, vec2<i32>(u_offsets2d))"},
 
         // OpImageSampleExplicitLod arrayed - using Lod and ConstOffset
         ImageAccessCase{
@@ -1994,7 +1992,7 @@
             R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d_array<f32>;)",
-            R"(textureSampleLevel(x_20, x_10, coords123.xy, i32(round(coords123.z)), 0.0f, vec2<i32>(3i, 4i)))"}));
+            R"(textureSampleLevel(x_20, x_10, coords123.xy, i32(round(coords123.z)), 0.0f, offsets2d))"}));
 
 INSTANTIATE_TEST_SUITE_P(
     ImageSampleExplicitLod_UsingGrad,
@@ -2021,15 +2019,14 @@
             R"(textureSampleGrad(x_20, x_10, coords123.xy, i32(round(coords123.z)), vf12, vf21))"},
 
         // OpImageSampleExplicitLod - using Grad and ConstOffset
-        ImageAccessCase{
-            "%float 2D 0 0 0 1 Unknown",
-            "%result = OpImageSampleExplicitLod "
-            "%v4float %sampled_image %coords12 Grad|ConstOffset "
-            "%vf12 %vf21 %offsets2d",
-            R"(@group(0) @binding(0) var x_10 : sampler;
+        ImageAccessCase{"%float 2D 0 0 0 1 Unknown",
+                        "%result = OpImageSampleExplicitLod "
+                        "%v4float %sampled_image %coords12 Grad|ConstOffset "
+                        "%vf12 %vf21 %offsets2d",
+                        R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-            R"(textureSampleGrad(x_20, x_10, coords12, vf12, vf21, vec2<i32>(3i, 4i)))"},
+                        R"(textureSampleGrad(x_20, x_10, coords12, vf12, vf21, offsets2d))"},
 
         // OpImageSampleExplicitLod - using Grad and unsigned ConstOffset
         ImageAccessCase{
@@ -2040,7 +2037,7 @@
             R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-            R"(textureSampleGrad(x_20, x_10, coords12, vf12, vf21, vec2<i32>(vec2<u32>(3u, 4u)))"},
+            R"(textureSampleGrad(x_20, x_10, coords12, vf12, vf21, vec2<i32>(u_offsets2d))"},
 
         // OpImageSampleExplicitLod arrayed - using Grad and ConstOffset
         ImageAccessCase{
@@ -2051,7 +2048,7 @@
             R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d_array<f32>;)",
-            R"(textureSampleGrad(x_20, x_10, coords123.xy, i32(round(coords123.z)), vf12, vf21, vec2<i32>(3i, 4i)))"},
+            R"(textureSampleGrad(x_20, x_10, coords123.xy, i32(round(coords123.z)), vf12, vf21, offsets2d))"},
 
         // OpImageSampleExplicitLod arrayed - using Grad and unsigned
         // ConstOffset
@@ -2063,7 +2060,7 @@
             R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d_array<f32>;)",
-            R"(textureSampleGrad(x_20, x_10, coords123.xy, i32(round(coords123.z)), vf12, vf21, vec2<i32>(vec2<u32>(3u, 4u))))"}));
+            R"(textureSampleGrad(x_20, x_10, coords123.xy, i32(round(coords123.z)), vf12, vf21, vec2<i32>(u_offsets2d)))"}));
 
 // Test crbug.com/378:
 // In WGSL, sampling from depth texture with explicit level of detail
@@ -2153,14 +2150,13 @@
         // OpImageSampleProjImplicitLod 2D with ConstOffset
         // (Don't need to test with 1D or 3D, as the hard part was the splatted
         // division.) This case tests handling of the ConstOffset
-        ImageAccessCase{
-            "%float 2D 0 0 0 1 Unknown",
-            "%result = OpImageSampleProjImplicitLod "
-            "%v4float %sampled_image %coords123 ConstOffset %offsets2d",
-            R"(@group(0) @binding(0) var x_10 : sampler;
+        ImageAccessCase{"%float 2D 0 0 0 1 Unknown",
+                        "%result = OpImageSampleProjImplicitLod "
+                        "%v4float %sampled_image %coords123 ConstOffset %offsets2d",
+                        R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-            R"(textureSample(x_20, x_10, (coords123.xy / coords123.z), vec2<i32>(3i, 4i)))"}));
+                        R"(textureSample(x_20, x_10, (coords123.xy / coords123.z), offsets2d))"}));
 
 INSTANTIATE_TEST_SUITE_P(
     ImageSampleProjImplicitLod_Bias,
@@ -2186,7 +2182,7 @@
             R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-            R"(textureSampleBias(x_20, x_10, (coords123.xy / coords123.z), 7.0f, vec2<i32>(3i, 4i)))"},
+            R"(textureSampleBias(x_20, x_10, (coords123.xy / coords123.z), 7.0f, offsets2d))"},
 
         // OpImageSampleProjImplicitLod with Bias and unsigned ConstOffset
         // Convert ConstOffset to signed
@@ -2198,7 +2194,7 @@
             R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-            R"(textureSampleBias(x_20, x_10, (coords123.xy / coords123.z), 7.0f, vec2<i32>(vec2<u32>(3u, 4u))))"}));
+            R"(textureSampleBias(x_20, x_10, (coords123.xy / coords123.z), 7.0f, vec2<i32>(u_offsets2d)))"}));
 
 INSTANTIATE_TEST_SUITE_P(
     ImageSampleProjExplicitLod_Lod,
@@ -2221,7 +2217,7 @@
             R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-            R"(textureSampleLevel(x_20, x_10, (coords123.xy / coords123.z), f1, vec2<i32>(3i, 4i)))"}));
+            R"(textureSampleLevel(x_20, x_10, (coords123.xy / coords123.z), f1, offsets2d))"}));
 
 INSTANTIATE_TEST_SUITE_P(
     ImageSampleProjExplicitLod_Grad,
@@ -2246,7 +2242,7 @@
             R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-            R"(textureSampleGrad(x_20, x_10, (coords123.xy / coords123.z), vf12, vf21, vec2<i32>(3i, 4i)))"}));
+            R"(textureSampleGrad(x_20, x_10, (coords123.xy / coords123.z), vf12, vf21, offsets2d))"}));
 
 INSTANTIATE_TEST_SUITE_P(
     // Ordinary (non-comparison) sampling on a depth texture.
@@ -2291,7 +2287,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_2d;
 )",
-            R"(textureSampleCompare(x_20, x_10, (coords123.xy / coords123.z), f1, vec2<i32>(3i, 4i)))"}));
+            R"(textureSampleCompare(x_20, x_10, (coords123.xy / coords123.z), f1, offsets2d))"}));
 
 INSTANTIATE_TEST_SUITE_P(
     DISABLED_ImageSampleProjDrefExplicitLod_Lod,
diff --git a/src/tint/reader/spirv/parser_impl_module_var_test.cc b/src/tint/reader/spirv/parser_impl_module_var_test.cc
index c731fbf..2ae0435 100644
--- a/src/tint/reader/spirv/parser_impl_module_var_test.cc
+++ b/src/tint/reader/spirv/parser_impl_module_var_test.cc
@@ -4132,6 +4132,35 @@
     EXPECT_EQ(got, expected) << got;
 }
 
+TEST_F(SpvModuleScopeVarParserTest, BuiltinPosition_MultiplePerVertexVariables) {
+    // This is not currently supported, so just make sure we produce a meaningful error instead of
+    // crashing.
+    const std::string assembly = R"(
+  OpCapability Shader
+  OpMemoryModel Logical Simple
+  OpEntryPoint Vertex %main "main" %1
+  OpDecorate %struct Block
+  OpMemberDecorate %struct 0 BuiltIn Position
+  %void = OpTypeVoid
+  %voidfn = OpTypeFunction %void
+  %f32 = OpTypeFloat 32
+  %vec4f = OpTypeVector %f32 4
+  %struct = OpTypeStruct %vec4f
+  %struct_out_ptr = OpTypePointer Output %struct
+  %1 = OpVariable %struct_out_ptr Output
+  %2 = OpVariable %struct_out_ptr Output
+  %main = OpFunction %void None %voidfn
+  %entry = OpLabel
+  OpReturn
+  OpFunctionEnd
+)";
+    auto p = parser(test::Assemble(assembly));
+
+    EXPECT_FALSE(p->Parse());
+    EXPECT_FALSE(p->success());
+    EXPECT_EQ(p->error(), "unsupported: multiple Position built-in variables in the same module");
+}
+
 TEST_F(SpvModuleScopeVarParserTest, Input_FlattenArray_OneLevel) {
     const std::string assembly = R"(
     OpCapability Shader
diff --git a/src/tint/reader/wgsl/lexer.cc b/src/tint/reader/wgsl/lexer.cc
index 1fc7e84..1e1dad4 100644
--- a/src/tint/reader/wgsl/lexer.cc
+++ b/src/tint/reader/wgsl/lexer.cc
@@ -39,7 +39,9 @@
               "tint::reader::wgsl requires the size of a std::string element "
               "to be a single byte");
 
-static constexpr size_t kDefaultListSize = 512;
+// A token is ~80bytes. The 4k here comes from looking at the number of tokens in the benchmark
+// programs and being a bit bigger then those need (atan2-const-eval is the outlier here).
+static constexpr size_t kDefaultListSize = 4092;
 
 bool read_blankspace(std::string_view str, size_t i, bool* is_blankspace, size_t* blankspace_size) {
     // See https://www.w3.org/TR/WGSL/#blankspace
@@ -95,8 +97,12 @@
 std::vector<Token> Lexer::Lex() {
     std::vector<Token> tokens;
     tokens.reserve(kDefaultListSize);
+
     while (true) {
         tokens.emplace_back(next());
+        if (tokens.back().IsEof() || tokens.back().IsError()) {
+            break;
+        }
 
         // If the token can be split, we insert a placeholder element(s) into the stream to hold the
         // split character.
@@ -104,11 +110,7 @@
         for (size_t i = 0; i < num_placeholders; i++) {
             auto src = tokens.back().source();
             src.range.begin.column++;
-            tokens.emplace_back(Token(Token::Type::kPlaceholder, src));
-        }
-
-        if (tokens.back().IsEof() || tokens.back().IsError()) {
-            break;
+            tokens.emplace_back(Token::Type::kPlaceholder, src);
         }
     }
     return tokens;
@@ -167,32 +169,32 @@
 }
 
 Token Lexer::next() {
-    if (auto t = skip_blankspace_and_comments(); !t.IsUninitialized()) {
-        return t;
+    if (auto t = skip_blankspace_and_comments(); t.has_value() && !t->IsUninitialized()) {
+        return std::move(t.value());
     }
 
-    if (auto t = try_hex_float(); !t.IsUninitialized()) {
-        return t;
+    if (auto t = try_hex_float(); t.has_value() && !t->IsUninitialized()) {
+        return std::move(t.value());
     }
 
-    if (auto t = try_hex_integer(); !t.IsUninitialized()) {
-        return t;
+    if (auto t = try_hex_integer(); t.has_value() && !t->IsUninitialized()) {
+        return std::move(t.value());
     }
 
-    if (auto t = try_float(); !t.IsUninitialized()) {
-        return t;
+    if (auto t = try_float(); t.has_value() && !t->IsUninitialized()) {
+        return std::move(t.value());
     }
 
-    if (auto t = try_integer(); !t.IsUninitialized()) {
-        return t;
+    if (auto t = try_integer(); t.has_value() && !t->IsUninitialized()) {
+        return std::move(t.value());
     }
 
-    if (auto t = try_ident(); !t.IsUninitialized()) {
-        return t;
+    if (auto t = try_ident(); t.has_value() && !t->IsUninitialized()) {
+        return std::move(t.value());
     }
 
-    if (auto t = try_punctuation(); !t.IsUninitialized()) {
-        return t;
+    if (auto t = try_punctuation(); t.has_value() && !t->IsUninitialized()) {
+        return std::move(t.value());
     }
 
     return {Token::Type::kError, begin_source(),
@@ -237,7 +239,7 @@
     return line()[pos] == ch;
 }
 
-Token Lexer::skip_blankspace_and_comments() {
+std::optional<Token> Lexer::skip_blankspace_and_comments() {
     for (;;) {
         auto loc = location_;
         while (!is_eof()) {
@@ -249,7 +251,7 @@
             bool is_blankspace;
             size_t blankspace_size;
             if (!read_blankspace(line(), pos(), &is_blankspace, &blankspace_size)) {
-                return {Token::Type::kError, begin_source(), "invalid UTF-8"};
+                return Token{Token::Type::kError, begin_source(), "invalid UTF-8"};
             }
             if (!is_blankspace) {
                 break;
@@ -259,7 +261,7 @@
         }
 
         auto t = skip_comment();
-        if (!t.IsUninitialized()) {
+        if (t.has_value() && !t->IsUninitialized()) {
             return t;
         }
 
@@ -270,18 +272,18 @@
         }
     }
     if (is_eof()) {
-        return {Token::Type::kEOF, begin_source()};
+        return Token{Token::Type::kEOF, begin_source()};
     }
 
     return {};
 }
 
-Token Lexer::skip_comment() {
+std::optional<Token> Lexer::skip_comment() {
     if (matches(pos(), "//")) {
         // Line comment: ignore everything until the end of line.
         while (!is_eol()) {
             if (is_null()) {
-                return {Token::Type::kError, begin_source(), "null character found"};
+                return Token{Token::Type::kError, begin_source(), "null character found"};
             }
             advance();
         }
@@ -311,20 +313,20 @@
                 // Newline: skip and update source location.
                 advance_line();
             } else if (is_null()) {
-                return {Token::Type::kError, begin_source(), "null character found"};
+                return Token{Token::Type::kError, begin_source(), "null character found"};
             } else {
                 // Anything else: skip and update source location.
                 advance();
             }
         }
         if (depth > 0) {
-            return {Token::Type::kError, source, "unterminated block comment"};
+            return Token{Token::Type::kError, source, "unterminated block comment"};
         }
     }
     return {};
 }
 
-Token Lexer::try_float() {
+std::optional<Token> Lexer::try_float() {
     auto start = pos();
     auto end = pos();
 
@@ -385,8 +387,8 @@
         // If an 'e' or 'E' was present, then the number part must also be present.
         if (!has_digits) {
             const auto str = std::string{substr(start, end - start)};
-            return {Token::Type::kError, source,
-                    "incomplete exponent for floating point literal: " + str};
+            return Token{Token::Type::kError, source,
+                         "incomplete exponent for floating point literal: " + str};
         }
     }
 
@@ -452,9 +454,9 @@
         if (!overflow && f) {
             advance(1);
             end_source(source);
-            return {Token::Type::kFloatLiteral_F, source, static_cast<double>(f.Get())};
+            return Token{Token::Type::kFloatLiteral_F, source, static_cast<double>(f.Get())};
         }
-        return {Token::Type::kError, source, "value cannot be represented as 'f32'"};
+        return Token{Token::Type::kError, source, "value cannot be represented as 'f32'"};
     }
 
     if (has_h_suffix) {
@@ -462,23 +464,24 @@
         if (!overflow && f) {
             advance(1);
             end_source(source);
-            return {Token::Type::kFloatLiteral_H, source, static_cast<double>(f.Get())};
+            return Token{Token::Type::kFloatLiteral_H, source, static_cast<double>(f.Get())};
         }
-        return {Token::Type::kError, source, "value cannot be represented as 'f16'"};
+        return Token{Token::Type::kError, source, "value cannot be represented as 'f16'"};
     }
 
     end_source(source);
 
     TINT_BEGIN_DISABLE_WARNING(FLOAT_EQUAL);
     if (overflow || value == HUGE_VAL || -value == HUGE_VAL) {
-        return {Token::Type::kError, source, "value cannot be represented as 'abstract-float'"};
+        return Token{Token::Type::kError, source,
+                     "value cannot be represented as 'abstract-float'"};
     } else {
-        return {Token::Type::kFloatLiteral, source, value};
+        return Token{Token::Type::kFloatLiteral, source, value};
     }
     TINT_END_DISABLE_WARNING(FLOAT_EQUAL);
 }
 
-Token Lexer::try_hex_float() {
+std::optional<Token> Lexer::try_hex_float() {
     constexpr uint64_t kExponentBits = 11;
     constexpr uint64_t kMantissaBits = 52;
     constexpr uint64_t kTotalBits = 1 + kExponentBits + kMantissaBits;
@@ -593,7 +596,8 @@
             // Skip leading 0s and the first 1
             if (seen_prior_one_bits) {
                 if (!set_next_mantissa_bit_to(v != 0, true)) {
-                    return {Token::Type::kError, source, "mantissa is too large for hex float"};
+                    return Token{Token::Type::kError, source,
+                                 "mantissa is too large for hex float"};
                 }
                 ++exponent;
             } else {
@@ -622,7 +626,8 @@
                 --exponent;
             } else {
                 if (!set_next_mantissa_bit_to(v != 0, false)) {
-                    return {Token::Type::kError, source, "mantissa is too large for hex float"};
+                    return Token{Token::Type::kError, source,
+                                 "mantissa is too large for hex float"};
                 }
             }
         }
@@ -663,7 +668,7 @@
             // Check if we've overflowed input_exponent. This only matters when
             // the mantissa is non-zero.
             if (!is_zero && (prev_exponent > input_exponent)) {
-                return {Token::Type::kError, source, "exponent is too large for hex float"};
+                return Token{Token::Type::kError, source, "exponent is too large for hex float"};
             }
             end++;
         }
@@ -680,7 +685,7 @@
         }
 
         if (!has_exponent_digits) {
-            return {Token::Type::kError, source, "expected an exponent value for hex float"};
+            return Token{Token::Type::kError, source, "expected an exponent value for hex float"};
         }
     }
 
@@ -696,7 +701,7 @@
         const uint64_t kIntMax = static_cast<uint64_t>(std::numeric_limits<int64_t>::max());
         const uint64_t kMaxInputExponent = kIntMax - kExponentBias;
         if (input_exponent > kMaxInputExponent) {
-            return {Token::Type::kError, source, "exponent is too large for hex float"};
+            return Token{Token::Type::kError, source, "exponent is too large for hex float"};
         }
 
         // Compute exponent so far
@@ -746,7 +751,7 @@
 
     if (signed_exponent >= kExponentMax || (signed_exponent == kExponentMax && mantissa != 0)) {
         std::string type = has_f_suffix ? "f32" : (has_h_suffix ? "f16" : "abstract-float");
-        return {Token::Type::kError, source, "value cannot be represented as '" + type + "'"};
+        return Token{Token::Type::kError, source, "value cannot be represented as '" + type + "'"};
     }
 
     // Combine sign, mantissa, and exponent
@@ -762,7 +767,7 @@
         // Check value fits in f32
         if (result_f64 < static_cast<double>(f32::kLowestValue) ||
             result_f64 > static_cast<double>(f32::kHighestValue)) {
-            return {Token::Type::kError, source, "value cannot be represented as 'f32'"};
+            return Token{Token::Type::kError, source, "value cannot be represented as 'f32'"};
         }
         // Check the value can be exactly represented, i.e. only high 23 mantissa bits are valid for
         // normal f32 values, and less for subnormal f32 values. The rest low mantissa bits must be
@@ -803,19 +808,21 @@
         } else if (abs_result_f64 != 0.0) {
             // The result is smaller than the smallest subnormal f32 value, but not equal to zero.
             // Such value will never be exactly represented by f32.
-            return {Token::Type::kError, source, "value cannot be exactly represented as 'f32'"};
+            return Token{Token::Type::kError, source,
+                         "value cannot be exactly represented as 'f32'"};
         }
         // Check the low 52-valid_mantissa_bits mantissa bits must be 0.
         TINT_ASSERT(Reader, (0 <= valid_mantissa_bits) && (valid_mantissa_bits <= 23));
         if (result_u64 & ((uint64_t(1) << (52 - valid_mantissa_bits)) - 1)) {
-            return {Token::Type::kError, source, "value cannot be exactly represented as 'f32'"};
+            return Token{Token::Type::kError, source,
+                         "value cannot be exactly represented as 'f32'"};
         }
-        return {Token::Type::kFloatLiteral_F, source, result_f64};
+        return Token{Token::Type::kFloatLiteral_F, source, result_f64};
     } else if (has_h_suffix) {
         // Check value fits in f16
         if (result_f64 < static_cast<double>(f16::kLowestValue) ||
             result_f64 > static_cast<double>(f16::kHighestValue)) {
-            return {Token::Type::kError, source, "value cannot be represented as 'f16'"};
+            return Token{Token::Type::kError, source, "value cannot be represented as 'f16'"};
         }
         // Check the value can be exactly represented, i.e. only high 10 mantissa bits are valid for
         // normal f16 values, and less for subnormal f16 values. The rest low mantissa bits must be
@@ -854,17 +861,19 @@
         } else if (abs_result_f64 != 0.0) {
             // The result is smaller than the smallest subnormal f16 value, but not equal to zero.
             // Such value will never be exactly represented by f16.
-            return {Token::Type::kError, source, "value cannot be exactly represented as 'f16'"};
+            return Token{Token::Type::kError, source,
+                         "value cannot be exactly represented as 'f16'"};
         }
         // Check the low 52-valid_mantissa_bits mantissa bits must be 0.
         TINT_ASSERT(Reader, (0 <= valid_mantissa_bits) && (valid_mantissa_bits <= 10));
         if (result_u64 & ((uint64_t(1) << (52 - valid_mantissa_bits)) - 1)) {
-            return {Token::Type::kError, source, "value cannot be exactly represented as 'f16'"};
+            return Token{Token::Type::kError, source,
+                         "value cannot be exactly represented as 'f16'"};
         }
-        return {Token::Type::kFloatLiteral_H, source, result_f64};
+        return Token{Token::Type::kFloatLiteral_H, source, result_f64};
     }
 
-    return {Token::Type::kFloatLiteral, source, result_f64};
+    return Token{Token::Type::kFloatLiteral, source, result_f64};
 }
 
 Token Lexer::build_token_from_int_if_possible(Source source,
@@ -911,7 +920,7 @@
     return {Token::Type::kIntLiteral, source, value};
 }
 
-Token Lexer::try_hex_integer() {
+std::optional<Token> Lexer::try_hex_integer() {
     auto start = pos();
     auto curr = start;
 
@@ -924,14 +933,14 @@
     }
 
     if (!is_hex(at(curr))) {
-        return {Token::Type::kError, source,
-                "integer or float hex literal has no significant digits"};
+        return Token{Token::Type::kError, source,
+                     "integer or float hex literal has no significant digits"};
     }
 
     return build_token_from_int_if_possible(source, curr, curr - start, 16);
 }
 
-Token Lexer::try_integer() {
+std::optional<Token> Lexer::try_integer() {
     auto start = pos();
     auto curr = start;
 
@@ -945,14 +954,14 @@
     // are not allowed.
     if (auto next = curr + 1; next < length()) {
         if (at(curr) == '0' && is_digit(at(next))) {
-            return {Token::Type::kError, source, "integer literal cannot have leading 0s"};
+            return Token{Token::Type::kError, source, "integer literal cannot have leading 0s"};
         }
     }
 
     return build_token_from_int_if_possible(source, start, 0, 10);
 }
 
-Token Lexer::try_ident() {
+std::optional<Token> Lexer::try_ident() {
     auto source = begin_source();
     auto start = pos();
 
@@ -962,7 +971,7 @@
         auto [code_point, n] = utils::utf8::Decode(utf8, length() - pos());
         if (n == 0) {
             advance();  // Skip the bad byte.
-            return {Token::Type::kError, source, "invalid UTF-8"};
+            return Token{Token::Type::kError, source, "invalid UTF-8"};
         }
         if (code_point != utils::CodePoint('_') && !code_point.IsXIDStart()) {
             return {};
@@ -977,7 +986,7 @@
         auto [code_point, n] = utils::utf8::Decode(utf8, line().size() - pos());
         if (n == 0) {
             advance();  // Skip the bad byte.
-            return {Token::Type::kError, source, "invalid UTF-8"};
+            return Token{Token::Type::kError, source, "invalid UTF-8"};
         }
         if (!code_point.IsXIDContinue()) {
             break;
@@ -999,15 +1008,14 @@
     auto str = substr(start, pos() - start);
     end_source(source);
 
-    auto t = check_keyword(source, str);
-    if (!t.IsUninitialized()) {
+    if (auto t = check_keyword(source, str); t.has_value() && !t->IsUninitialized()) {
         return t;
     }
 
-    return {Token::Type::kIdentifier, source, str};
+    return Token{Token::Type::kIdentifier, source, str};
 }
 
-Token Lexer::try_punctuation() {
+std::optional<Token> Lexer::try_punctuation() {
     auto source = begin_source();
     auto type = Token::Type::kUninitialized;
 
@@ -1177,97 +1185,99 @@
             type = Token::Type::kXor;
             advance(1);
         }
+    } else {
+        return {};
     }
 
     end_source(source);
 
-    return {type, source};
+    return Token{type, source};
 }
 
-Token Lexer::check_keyword(const Source& source, std::string_view str) {
+std::optional<Token> Lexer::check_keyword(const Source& source, std::string_view str) {
     if (str == "alias") {
-        return {Token::Type::kAlias, source, "alias"};
+        return Token{Token::Type::kAlias, source, "alias"};
     }
     if (str == "bitcast") {
-        return {Token::Type::kBitcast, source, "bitcast"};
+        return Token{Token::Type::kBitcast, source, "bitcast"};
     }
     if (str == "break") {
-        return {Token::Type::kBreak, source, "break"};
+        return Token{Token::Type::kBreak, source, "break"};
     }
     if (str == "case") {
-        return {Token::Type::kCase, source, "case"};
+        return Token{Token::Type::kCase, source, "case"};
     }
     if (str == "const") {
-        return {Token::Type::kConst, source, "const"};
+        return Token{Token::Type::kConst, source, "const"};
     }
     if (str == "const_assert") {
-        return {Token::Type::kConstAssert, source, "const_assert"};
+        return Token{Token::Type::kConstAssert, source, "const_assert"};
     }
     if (str == "continue") {
-        return {Token::Type::kContinue, source, "continue"};
+        return Token{Token::Type::kContinue, source, "continue"};
     }
     if (str == "continuing") {
-        return {Token::Type::kContinuing, source, "continuing"};
+        return Token{Token::Type::kContinuing, source, "continuing"};
     }
     if (str == "diagnostic") {
-        return {Token::Type::kDiagnostic, source, "diagnostic"};
+        return Token{Token::Type::kDiagnostic, source, "diagnostic"};
     }
     if (str == "discard") {
-        return {Token::Type::kDiscard, source, "discard"};
+        return Token{Token::Type::kDiscard, source, "discard"};
     }
     if (str == "default") {
-        return {Token::Type::kDefault, source, "default"};
+        return Token{Token::Type::kDefault, source, "default"};
     }
     if (str == "else") {
-        return {Token::Type::kElse, source, "else"};
+        return Token{Token::Type::kElse, source, "else"};
     }
     if (str == "enable") {
-        return {Token::Type::kEnable, source, "enable"};
+        return Token{Token::Type::kEnable, source, "enable"};
     }
     if (str == "fallthrough") {
-        return {Token::Type::kFallthrough, source, "fallthrough"};
+        return Token{Token::Type::kFallthrough, source, "fallthrough"};
     }
     if (str == "false") {
-        return {Token::Type::kFalse, source, "false"};
+        return Token{Token::Type::kFalse, source, "false"};
     }
     if (str == "fn") {
-        return {Token::Type::kFn, source, "fn"};
+        return Token{Token::Type::kFn, source, "fn"};
     }
     if (str == "for") {
-        return {Token::Type::kFor, source, "for"};
+        return Token{Token::Type::kFor, source, "for"};
     }
     if (str == "if") {
-        return {Token::Type::kIf, source, "if"};
+        return Token{Token::Type::kIf, source, "if"};
     }
     if (str == "let") {
-        return {Token::Type::kLet, source, "let"};
+        return Token{Token::Type::kLet, source, "let"};
     }
     if (str == "loop") {
-        return {Token::Type::kLoop, source, "loop"};
+        return Token{Token::Type::kLoop, source, "loop"};
     }
     if (str == "override") {
-        return {Token::Type::kOverride, source, "override"};
+        return Token{Token::Type::kOverride, source, "override"};
     }
     if (str == "return") {
-        return {Token::Type::kReturn, source, "return"};
+        return Token{Token::Type::kReturn, source, "return"};
     }
     if (str == "requires") {
-        return {Token::Type::kRequires, source, "requires"};
+        return Token{Token::Type::kRequires, source, "requires"};
     }
     if (str == "struct") {
-        return {Token::Type::kStruct, source, "struct"};
+        return Token{Token::Type::kStruct, source, "struct"};
     }
     if (str == "switch") {
-        return {Token::Type::kSwitch, source, "switch"};
+        return Token{Token::Type::kSwitch, source, "switch"};
     }
     if (str == "true") {
-        return {Token::Type::kTrue, source, "true"};
+        return Token{Token::Type::kTrue, source, "true"};
     }
     if (str == "var") {
-        return {Token::Type::kVar, source, "var"};
+        return Token{Token::Type::kVar, source, "var"};
     }
     if (str == "while") {
-        return {Token::Type::kWhile, source, "while"};
+        return Token{Token::Type::kWhile, source, "while"};
     }
     return {};
 }
diff --git a/src/tint/reader/wgsl/lexer.h b/src/tint/reader/wgsl/lexer.h
index ac41dae..e56a79a 100644
--- a/src/tint/reader/wgsl/lexer.h
+++ b/src/tint/reader/wgsl/lexer.h
@@ -15,6 +15,7 @@
 #ifndef SRC_TINT_READER_WGSL_LEXER_H_
 #define SRC_TINT_READER_WGSL_LEXER_H_
 
+#include <optional>
 #include <string>
 #include <vector>
 
@@ -40,19 +41,19 @@
 
     /// Advances past blankspace and comments, if present at the current position.
     /// @returns error token, EOF, or uninitialized
-    Token skip_blankspace_and_comments();
+    std::optional<Token> skip_blankspace_and_comments();
     /// Advances past a comment at the current position, if one exists.
     /// Returns an error if there was an unterminated block comment,
     /// or a null character was present.
     /// @returns uninitialized token on success, or error
-    Token skip_comment();
+    std::optional<Token> skip_comment();
 
     Token build_token_from_int_if_possible(Source source,
                                            size_t start,
                                            size_t prefix_count,
                                            int32_t base);
 
-    Token check_keyword(const Source&, std::string_view);
+    std::optional<Token> check_keyword(const Source&, std::string_view);
 
     /// The try_* methods have the following in common:
     /// - They assume there is at least one character to be consumed,
@@ -62,12 +63,12 @@
     /// - Some can return an error token.
     /// - Otherwise they return an uninitialized token when they did not
     ///   match a token of the specfied kind.
-    Token try_float();
-    Token try_hex_float();
-    Token try_hex_integer();
-    Token try_ident();
-    Token try_integer();
-    Token try_punctuation();
+    std::optional<Token> try_float();
+    std::optional<Token> try_hex_float();
+    std::optional<Token> try_hex_integer();
+    std::optional<Token> try_ident();
+    std::optional<Token> try_integer();
+    std::optional<Token> try_punctuation();
 
     Source begin_source() const;
     void end_source(Source&) const;
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 19571a7..58050b9 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -3115,13 +3115,14 @@
 }
 
 sem::ValueExpression* Resolver::MemberAccessor(const ast::MemberAccessorExpression* expr) {
-    auto* structure = sem_.TypeOf(expr->object);
-    auto* storage_ty = structure->UnwrapRef();
     auto* object = sem_.GetVal(expr->object);
     if (!object) {
         return nullptr;
     }
 
+    auto* object_ty = object->Type();
+    auto* storage_ty = object_ty->UnwrapRef();
+
     auto* root_ident = object->RootIdentifier();
 
     const type::Type* ty = nullptr;
@@ -3152,7 +3153,7 @@
             ty = member->Type();
 
             // If we're extracting from a reference, we return a reference.
-            if (auto* ref = structure->As<type::Reference>()) {
+            if (auto* ref = object_ty->As<type::Reference>()) {
                 ty = builder_->create<type::Reference>(ty, ref->AddressSpace(), ref->Access());
             }
 
@@ -3221,7 +3222,7 @@
                 // A single element swizzle is just the type of the vector.
                 ty = vec->type();
                 // If we're extracting from a reference, we return a reference.
-                if (auto* ref = structure->As<type::Reference>()) {
+                if (auto* ref = object_ty->As<type::Reference>()) {
                     ty = builder_->create<type::Reference>(ty, ref->AddressSpace(), ref->Access());
                 }
             } else {
diff --git a/src/tint/resolver/resolver_test.cc b/src/tint/resolver/resolver_test.cc
index a601ef0..5df2fae 100644
--- a/src/tint/resolver/resolver_test.cc
+++ b/src/tint/resolver/resolver_test.cc
@@ -1239,6 +1239,15 @@
     EXPECT_EQ(func_sem->WorkgroupSize()[2], 3u);
 }
 
+TEST_F(ResolverTest, Expr_MemberAccessor_Type) {
+    auto* mem = MemberAccessor(Ident(Source{{12, 34}}, "f32"), "member");
+    WrapInFunction(mem);
+
+    EXPECT_FALSE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), R"(12:34 error: cannot use type 'f32' as value
+12:34 note: are you missing '()' for value constructor?)");
+}
+
 TEST_F(ResolverTest, Expr_MemberAccessor_Struct) {
     auto* st = Structure(
         "S", utils::Vector{Member("first_member", ty.i32()), Member("second_member", ty.f32())});
diff --git a/src/tint/resolver/sem_helper.cc b/src/tint/resolver/sem_helper.cc
index 2357ce1..dd2341f 100644
--- a/src/tint/resolver/sem_helper.cc
+++ b/src/tint/resolver/sem_helper.cc
@@ -105,8 +105,7 @@
     ErrorUnexpectedExprKind(expr, "value");
     if (auto* ty_expr = expr->As<sem::TypeExpression>()) {
         if (auto* ident = ty_expr->Declaration()->As<ast::IdentifierExpression>()) {
-            AddNote("are you missing '()' for value constructor?",
-                    Source{{ident->source.range.end}});
+            AddNote("are you missing '()' for value constructor?", ident->source.End());
         }
     }
 }
diff --git a/src/tint/transform/module_scope_var_to_entry_point_param.cc b/src/tint/transform/module_scope_var_to_entry_point_param.cc
index e82b1bd..a394ef6 100644
--- a/src/tint/transform/module_scope_var_to_entry_point_param.cc
+++ b/src/tint/transform/module_scope_var_to_entry_point_param.cc
@@ -85,6 +85,11 @@
                 return;
             }
 
+            if (!str->Declaration()) {
+                // The struct is a built-in structure that we do not need to declare.
+                return;
+            }
+
             // Recurse into members.
             for (auto* member : str->Members()) {
                 CloneStructTypes(member->Type());
diff --git a/src/tint/transform/transform.cc b/src/tint/transform/transform.cc
index 73ee3ae..2002d02 100644
--- a/src/tint/transform/transform.cc
+++ b/src/tint/transform/transform.cc
@@ -144,7 +144,7 @@
         return ctx.dst->ty.array(el, u32(count.value()), std::move(attrs));
     }
     if (auto* s = ty->As<sem::Struct>()) {
-        return ctx.dst->ty(ctx.Clone(s->Declaration()->name->symbol));
+        return ctx.dst->ty(ctx.Clone(s->Name()));
     }
     if (auto* s = ty->As<type::Reference>()) {
         return CreateASTTypeFor(ctx, s->StoreType());
diff --git a/src/tint/writer/msl/generator_impl.cc b/src/tint/writer/msl/generator_impl.cc
index df9ff4b..0435918 100644
--- a/src/tint/writer/msl/generator_impl.cc
+++ b/src/tint/writer/msl/generator_impl.cc
@@ -347,7 +347,7 @@
         // WGSL can ignore the invariant attribute on pre MSL 2.1 devices.
         // See: https://github.com/gpuweb/gpuweb/issues/893#issuecomment-745537465
         line(&helpers_) << "#if __METAL_VERSION__ >= 210";
-        line(&helpers_) << "#define " << invariant_define_name_ << " @invariant";
+        line(&helpers_) << "#define " << invariant_define_name_ << " [[invariant]]";
         line(&helpers_) << "#else";
         line(&helpers_) << "#define " << invariant_define_name_;
         line(&helpers_) << "#endif";
diff --git a/src/tint/writer/msl/generator_impl_test.cc b/src/tint/writer/msl/generator_impl_test.cc
index 4772d46..41333db 100644
--- a/src/tint/writer/msl/generator_impl_test.cc
+++ b/src/tint/writer/msl/generator_impl_test.cc
@@ -119,7 +119,7 @@
 using namespace metal;
 
 #if __METAL_VERSION__ >= 210
-#define TINT_INVARIANT @invariant
+#define TINT_INVARIANT [[invariant]]
 #else
 #define TINT_INVARIANT
 #endif