[tint][ir] Add ValueToLet transform

Moves values into local variable declarations, where necessary for textual writers.

Change-Id: Ia3b11233904a8f17fb861d71bcf1cf12de9390c6
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/162541
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/lang/core/ir/let.h b/src/tint/lang/core/ir/let.h
index c55ed3c..a2fe222 100644
--- a/src/tint/lang/core/ir/let.h
+++ b/src/tint/lang/core/ir/let.h
@@ -49,6 +49,9 @@
     /// @copydoc Instruction::Clone()
     Let* Clone(CloneContext& ctx) override;
 
+    /// @param value the new let value
+    void SetValue(ir::Value* value) { SetOperand(kValueOperandOffset, value); }
+
     /// @returns the value
     ir::Value* Value() { return operands_[kValueOperandOffset]; }
 
diff --git a/src/tint/lang/core/ir/module.cc b/src/tint/lang/core/ir/module.cc
index 8719e2b..4738c14 100644
--- a/src/tint/lang/core/ir/module.cc
+++ b/src/tint/lang/core/ir/module.cc
@@ -67,4 +67,8 @@
     value_to_name_.Replace(value, name);
 }
 
+void Module::ClearName(Value* value) {
+    value_to_name_.Remove(value);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/module.h b/src/tint/lang/core/ir/module.h
index a792254..fba89fb 100644
--- a/src/tint/lang/core/ir/module.h
+++ b/src/tint/lang/core/ir/module.h
@@ -92,6 +92,10 @@
     /// @param name the desired name of the value
     void SetName(Value* value, Symbol name);
 
+    /// Removes the name from @p value
+    /// @param value the value to remove the name from
+    void ClearName(Value* value);
+
     /// @return the type manager for the module
     core::type::Manager& Types() { return constant_values.types; }
 
diff --git a/src/tint/lang/core/ir/transform/BUILD.bazel b/src/tint/lang/core/ir/transform/BUILD.bazel
index 8870a6b..b93dfc2 100644
--- a/src/tint/lang/core/ir/transform/BUILD.bazel
+++ b/src/tint/lang/core/ir/transform/BUILD.bazel
@@ -54,6 +54,7 @@
     "robustness.cc",
     "shader_io.cc",
     "std140.cc",
+    "value_to_let.cc",
     "vectorize_scalar_matrix_constructors.cc",
     "zero_init_workgroup_memory.cc",
   ],
@@ -73,6 +74,7 @@
     "robustness.h",
     "shader_io.h",
     "std140.h",
+    "value_to_let.h",
     "vectorize_scalar_matrix_constructors.h",
     "zero_init_workgroup_memory.h",
   ],
@@ -120,6 +122,7 @@
     "preserve_padding_test.cc",
     "robustness_test.cc",
     "std140_test.cc",
+    "value_to_let_test.cc",
     "vectorize_scalar_matrix_constructors_test.cc",
     "zero_init_workgroup_memory_test.cc",
   ] + select({
diff --git a/src/tint/lang/core/ir/transform/BUILD.cmake b/src/tint/lang/core/ir/transform/BUILD.cmake
index 3465e11..f1d92a8 100644
--- a/src/tint/lang/core/ir/transform/BUILD.cmake
+++ b/src/tint/lang/core/ir/transform/BUILD.cmake
@@ -69,6 +69,8 @@
   lang/core/ir/transform/shader_io.h
   lang/core/ir/transform/std140.cc
   lang/core/ir/transform/std140.h
+  lang/core/ir/transform/value_to_let.cc
+  lang/core/ir/transform/value_to_let.h
   lang/core/ir/transform/vectorize_scalar_matrix_constructors.cc
   lang/core/ir/transform/vectorize_scalar_matrix_constructors.h
   lang/core/ir/transform/zero_init_workgroup_memory.cc
@@ -118,6 +120,7 @@
   lang/core/ir/transform/preserve_padding_test.cc
   lang/core/ir/transform/robustness_test.cc
   lang/core/ir/transform/std140_test.cc
+  lang/core/ir/transform/value_to_let_test.cc
   lang/core/ir/transform/vectorize_scalar_matrix_constructors_test.cc
   lang/core/ir/transform/zero_init_workgroup_memory_test.cc
 )
diff --git a/src/tint/lang/core/ir/transform/BUILD.gn b/src/tint/lang/core/ir/transform/BUILD.gn
index 7343823..1e7245e 100644
--- a/src/tint/lang/core/ir/transform/BUILD.gn
+++ b/src/tint/lang/core/ir/transform/BUILD.gn
@@ -74,6 +74,8 @@
     "shader_io.h",
     "std140.cc",
     "std140.h",
+    "value_to_let.cc",
+    "value_to_let.h",
     "vectorize_scalar_matrix_constructors.cc",
     "vectorize_scalar_matrix_constructors.h",
     "zero_init_workgroup_memory.cc",
@@ -120,6 +122,7 @@
       "preserve_padding_test.cc",
       "robustness_test.cc",
       "std140_test.cc",
+      "value_to_let_test.cc",
       "vectorize_scalar_matrix_constructors_test.cc",
       "zero_init_workgroup_memory_test.cc",
     ]
diff --git a/src/tint/lang/core/ir/transform/value_to_let.cc b/src/tint/lang/core/ir/transform/value_to_let.cc
new file mode 100644
index 0000000..7580900
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/value_to_let.cc
@@ -0,0 +1,186 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/transform/value_to_let.h"
+
+#include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/validator.h"
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+namespace tint::core::ir::transform {
+
+namespace {
+
+/// Access is an enumerator of memory access operations
+enum class Access : uint8_t { kLoad, kStore };
+/// Accesses is a set of of Access
+using Accesses = EnumSet<Access>;
+
+/// @returns the accesses that may be performed by the instruction @p inst
+Accesses AccessesFor(ir::Instruction* inst) {
+    return tint::Switch<Accesses>(
+        inst,                                                           //
+        [&](const ir::Load*) { return Access::kLoad; },                 //
+        [&](const ir::LoadVectorElement*) { return Access::kLoad; },    //
+        [&](const ir::Store*) { return Access::kStore; },               //
+        [&](const ir::StoreVectorElement*) { return Access::kStore; },  //
+        [&](const ir::Call*) {
+            return Accesses{Access::kLoad, Access::kStore};
+        },
+        [&](Default) { return Accesses{}; });
+}
+
+/// PIMPL state for the transform.
+struct State {
+    /// The IR module.
+    Module& ir;
+
+    /// The IR builder.
+    Builder b{ir};
+
+    /// The type manager.
+    core::type::Manager& ty{ir.Types()};
+
+    /// Process the module.
+    void Process() {
+        // Process each block.
+        for (auto* block : ir.blocks.Objects()) {
+            Process(block);
+        }
+    }
+
+  private:
+    void Process(ir::Block* block) {
+        // A set of possibly-inlinable values returned by a instructions that has not yet been
+        // marked-for or ruled-out-for inlining.
+        Hashset<ir::InstructionResult*, 32> pending_resolution;
+        // The accesses of the values in pending_resolution.
+        Access pending_access = Access::kLoad;
+
+        auto put_pending_in_lets = [&] {
+            for (auto* pending : pending_resolution) {
+                PutInLet(pending);
+            }
+            pending_resolution.Clear();
+        };
+
+        auto maybe_put_in_let = [&](auto* inst) {
+            if (auto* result = inst->Result(0)) {
+                auto& usages = result->Usages();
+                switch (usages.Count()) {
+                    case 0:  // No usage
+                        break;
+                    case 1: {  // Single usage
+                        auto* usage = (*usages.begin()).instruction;
+                        if (usage->Block() == inst->Block()) {
+                            // Usage in same block. Assign to pending_resolution, as we don't
+                            // know whether its safe to inline yet.
+                            pending_resolution.Add(result);
+                        } else {
+                            // Usage from another block. Cannot inline.
+                            inst = PutInLet(result);
+                        }
+                        break;
+                    }
+                    default:  // Value has multiple usages. Cannot inline.
+                        inst = PutInLet(result);
+                        break;
+                }
+            }
+        };
+
+        for (ir::Instruction* inst = block->Front(); inst; inst = inst->next) {
+            // This transform assumes that all multi-result instructions have been replaced
+            TINT_ASSERT(inst->Results().Length() < 2);
+
+            // The memory accesses of this instruction
+            auto accesses = AccessesFor(inst);
+
+            for (auto* operand : inst->Operands()) {
+                // If the operand is in pending_resolution, then we know it has a single use and
+                // because it hasn't been removed with put_pending_in_lets(), we know its safe to
+                // inline without breaking access ordering. By inlining the operand, we are pulling
+                // the operand's instruction into the same statement as this instruction, so this
+                // instruction adopts the access of the operand.
+                if (auto* result = As<InstructionResult>(operand)) {
+                    if (pending_resolution.Remove(result)) {
+                        // Var and Let are always statements, and so can never be inlined. As such,
+                        // they do not need to propagate the pending resolution through them.
+                        if (!inst->IsAnyOf<Var, Let>()) {
+                            accesses.Add(pending_access);
+                        }
+                    }
+                }
+            }
+
+            if (accesses.Contains(Access::kStore)) {  // Note: Also handles load + store
+                put_pending_in_lets();
+                maybe_put_in_let(inst);
+            } else if (accesses.Contains(Access::kLoad)) {
+                if (pending_access != Access::kLoad) {
+                    put_pending_in_lets();
+                    pending_access = Access::kLoad;
+                }
+                maybe_put_in_let(inst);
+            }
+        }
+    }
+
+    /// PutInLet places the value into a new 'let' instruction, immediately after the value's
+    /// instruction
+    /// @param value the value to place into the 'let'
+    /// @return the created 'let' instruction.
+    ir::Let* PutInLet(ir::InstructionResult* value) {
+        auto* inst = value->Instruction();
+        auto* let = b.Let(value->Type());
+        value->ReplaceAllUsesWith(let->Result(0));
+        let->SetValue(value);
+        let->InsertAfter(inst);
+        if (auto name = b.ir.NameOf(value); name.IsValid()) {
+            b.ir.SetName(let->Result(0), name);
+            b.ir.ClearName(value);
+        }
+        return let;
+    }
+};
+
+}  // namespace
+
+Result<SuccessType> ValueToLet(Module& ir) {
+    auto result = ValidateAndDumpIfNeeded(ir, "ValueToLet transform");
+    if (!result) {
+        return result;
+    }
+
+    State{ir}.Process();
+
+    return Success;
+}
+
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/value_to_let.h b/src/tint/lang/core/ir/transform/value_to_let.h
new file mode 100644
index 0000000..895bc09
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/value_to_let.h
@@ -0,0 +1,54 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_CORE_IR_TRANSFORM_VALUE_TO_LET_H_
+#define SRC_TINT_LANG_CORE_IR_TRANSFORM_VALUE_TO_LET_H_
+
+#include "src/tint/utils/result/result.h"
+
+// Forward declarations.
+namespace tint::core::ir {
+class Module;
+}
+
+namespace tint::core::ir::transform {
+
+/// ValueToLet is a transform that moves "non-inlinable" instruction values to let instructions.
+/// An expression is considered "non-inlinable" if any of the the following are true:
+/// * The value has multiple uses.
+/// * The value's instruction is a load that when inlined would cross a store instruction.
+/// * The value's instruction is a store instruction that when inlined would cross a load or store
+///   instruction.
+/// * The value is used in a block different to the value's instruction.
+///
+/// @param module the module to transform
+/// @returns error diagnostics on failure
+Result<SuccessType> ValueToLet(Module& module);
+
+}  // namespace tint::core::ir::transform
+
+#endif  // SRC_TINT_LANG_CORE_IR_TRANSFORM_VALUE_TO_LET_H_
diff --git a/src/tint/lang/core/ir/transform/value_to_let_test.cc b/src/tint/lang/core/ir/transform/value_to_let_test.cc
new file mode 100644
index 0000000..91e04d9
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/value_to_let_test.cc
@@ -0,0 +1,637 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/transform/value_to_let.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+
+namespace tint::core::ir::transform {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+using IR_ValueToLetTest = TransformTest;
+
+TEST_F(IR_ValueToLetTest, Empty) {
+    auto* expect = R"(
+)";
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, NoModify_Blah) {
+    auto* func = b.Function("F", ty.void_());
+    b.Append(func->Block(), [&] { b.Return(func); });
+
+    auto* src = R"(
+%F = func():void -> %b1 {
+  %b1 = block {
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, NoModify_Unsequenced) {
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* x = b.Let("x", 1_i);
+        auto* y = b.Let("y", 2_i);
+        auto* z = b.Let("z", b.Add<i32>(x, y));
+        b.Return(fn, z);
+    });
+
+    auto* src = R"(
+%F = func():i32 -> %b1 {
+  %b1 = block {
+    %x:i32 = let 1i
+    %y:i32 = let 2i
+    %4:i32 = add %x, %y
+    %z:i32 = let %4
+    ret %z
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+TEST_F(IR_ValueToLetTest, NoModify_SequencedValueUsedWithNonSequenced) {
+    auto* i = b.Var<private_, i32>("i");
+    b.ir.root_block->Append(i);
+
+    auto* p = b.FunctionParam<i32>("p");
+    auto* rmw = b.Function("rmw", ty.i32());
+    rmw->SetParams({p});
+    b.Append(rmw->Block(), [&] {
+        auto* v = b.Let("v", b.Add<i32>(b.Load(i), p));
+        b.Store(i, v);
+        b.Return(rmw, v);
+    });
+
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* x = b.Name("x", b.Call(rmw, 1_i));
+        // select is called with one, inlinable sequenced operand and two non-sequenced values.
+        auto* y = b.Name("y", b.Call<i32>(core::BuiltinFn::kSelect, 2_i, x, false));
+        b.Return(fn, y);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %i:ptr<private, i32, read_write> = var
+}
+
+%rmw = func(%p:i32):i32 -> %b2 {
+  %b2 = block {
+    %4:i32 = load %i
+    %5:i32 = add %4, %p
+    %v:i32 = let %5
+    store %i, %v
+    ret %v
+  }
+}
+%F = func():i32 -> %b3 {
+  %b3 = block {
+    %x:i32 = call %rmw, 1i
+    %y:i32 = select 2i, %x, false
+    ret %y
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, NoModify_Inlinable_NestedCalls) {
+    auto* i = b.Var<private_, i32>("i");
+    b.ir.root_block->Append(i);
+
+    auto* p = b.FunctionParam<i32>("p");
+    auto* rmw = b.Function("rmw", ty.i32());
+    rmw->SetParams({p});
+    b.Append(rmw->Block(), [&] {
+        auto* v = b.Let("v", b.Add<i32>(b.Load(i), p));
+        b.Store(i, v);
+        b.Return(rmw, v);
+    });
+
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* x = b.Name("x", b.Call(rmw, 1_i));
+        auto* y = b.Name("y", b.Call(rmw, x));
+        auto* z = b.Name("z", b.Call(rmw, y));
+        b.Return(fn, z);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %i:ptr<private, i32, read_write> = var
+}
+
+%rmw = func(%p:i32):i32 -> %b2 {
+  %b2 = block {
+    %4:i32 = load %i
+    %5:i32 = add %4, %p
+    %v:i32 = let %5
+    store %i, %v
+    ret %v
+  }
+}
+%F = func():i32 -> %b3 {
+  %b3 = block {
+    %x:i32 = call %rmw, 1i
+    %y:i32 = call %rmw, %x
+    %z:i32 = call %rmw, %y
+    ret %z
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, NoModify_LetUsedTwice) {
+    auto* i = b.Var<private_, i32>("i");
+    b.ir.root_block->Append(i);
+
+    auto* p = b.FunctionParam<i32>("p");
+    auto* rmw = b.Function("rmw", ty.i32());
+    rmw->SetParams({p});
+    b.Append(rmw->Block(), [&] {
+        auto* v = b.Let("v", b.Add<i32>(b.Load(i), p));
+        b.Store(i, v);
+        b.Return(rmw, v);
+    });
+
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        // No need to create more lets, as these are already in lets
+        auto* x = b.Let("x", b.Call(rmw, 1_i));
+        auto* y = b.Name("y", b.Add<i32>(x, x));
+        b.Return(fn, y);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %i:ptr<private, i32, read_write> = var
+}
+
+%rmw = func(%p:i32):i32 -> %b2 {
+  %b2 = block {
+    %4:i32 = load %i
+    %5:i32 = add %4, %p
+    %v:i32 = let %5
+    store %i, %v
+    ret %v
+  }
+}
+%F = func():i32 -> %b3 {
+  %b3 = block {
+    %8:i32 = call %rmw, 1i
+    %x:i32 = let %8
+    %y:i32 = add %x, %x
+    ret %y
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, NoModify_VarUsedTwice) {
+    auto* p = b.FunctionParam<ptr<function, i32, read_write>>("p");
+    auto* fn_g = b.Function("g", ty.i32());
+    fn_g->SetParams({p});
+    b.Append(fn_g->Block(), [&] { b.Return(fn_g, b.Load(p)); });
+
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* v = b.Var<function, i32>("v");
+        auto* x = b.Let("x", b.Call(fn_g, v));
+        auto* y = b.Let("y", b.Call(fn_g, v));
+        b.Return(fn, b.Add<i32>(x, y));
+    });
+
+    auto* src = R"(
+%g = func(%p:ptr<function, i32, read_write>):i32 -> %b1 {
+  %b1 = block {
+    %3:i32 = load %p
+    ret %3
+  }
+}
+%F = func():i32 -> %b2 {
+  %b2 = block {
+    %v:ptr<function, i32, read_write> = var
+    %6:i32 = call %g, %v
+    %x:i32 = let %6
+    %8:i32 = call %g, %v
+    %y:i32 = let %8
+    %10:i32 = add %x, %y
+    ret %10
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, VarLoadUsedTwice) {
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* v = b.Var<function, i32>("v");
+        auto* l = b.Name("l", b.Load(v));
+        b.Return(fn, b.Add<i32>(l, l));
+    });
+
+    auto* src = R"(
+%F = func():i32 -> %b1 {
+  %b1 = block {
+    %v:ptr<function, i32, read_write> = var
+    %l:i32 = load %v
+    %4:i32 = add %l, %l
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%F = func():i32 -> %b1 {
+  %b1 = block {
+    %v:ptr<function, i32, read_write> = var
+    %3:i32 = load %v
+    %l:i32 = let %3
+    %5:i32 = add %l, %l
+    ret %5
+  }
+}
+)";
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, VarLoad_ThenStore_ThenUse) {
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* v = b.Var<function, i32>("v");
+        auto* l = b.Name("l", b.Load(v));
+        b.Store(v, 1_i);
+        b.Return(fn, l);
+    });
+
+    auto* src = R"(
+%F = func():i32 -> %b1 {
+  %b1 = block {
+    %v:ptr<function, i32, read_write> = var
+    %l:i32 = load %v
+    store %v, 1i
+    ret %l
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%F = func():i32 -> %b1 {
+  %b1 = block {
+    %v:ptr<function, i32, read_write> = var
+    %3:i32 = load %v
+    %l:i32 = let %3
+    store %v, 1i
+    ret %l
+  }
+}
+)";
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, TwoCalls_ThenUseReturnValues) {
+    auto* i = b.Var<private_, i32>("i");
+    b.ir.root_block->Append(i);
+
+    auto* p = b.FunctionParam<i32>("p");
+    auto* rmw = b.Function("rmw", ty.i32());
+    rmw->SetParams({p});
+    b.Append(rmw->Block(), [&] {
+        auto* v = b.Let("v", b.Add<i32>(b.Load(i), p));
+        b.Store(i, v);
+        b.Return(rmw, v);
+    });
+
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* x = b.Name("x", b.Call(rmw, 1_i));
+        auto* y = b.Name("y", b.Call(rmw, 2_i));
+        auto* z = b.Name("z", b.Add<i32>(x, y));
+        b.Return(fn, z);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %i:ptr<private, i32, read_write> = var
+}
+
+%rmw = func(%p:i32):i32 -> %b2 {
+  %b2 = block {
+    %4:i32 = load %i
+    %5:i32 = add %4, %p
+    %v:i32 = let %5
+    store %i, %v
+    ret %v
+  }
+}
+%F = func():i32 -> %b3 {
+  %b3 = block {
+    %x:i32 = call %rmw, 1i
+    %y:i32 = call %rmw, 2i
+    %z:i32 = add %x, %y
+    ret %z
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %i:ptr<private, i32, read_write> = var
+}
+
+%rmw = func(%p:i32):i32 -> %b2 {
+  %b2 = block {
+    %4:i32 = load %i
+    %5:i32 = add %4, %p
+    %v:i32 = let %5
+    store %i, %v
+    ret %v
+  }
+}
+%F = func():i32 -> %b3 {
+  %b3 = block {
+    %8:i32 = call %rmw, 1i
+    %x:i32 = let %8
+    %y:i32 = call %rmw, 2i
+    %z:i32 = add %x, %y
+    ret %z
+  }
+}
+)";
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+
+    Run(ValueToLet);  // running a second time should be no-op
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, SequencedUsedInDifferentBlock) {
+    auto* i = b.Var<private_, i32>("i");
+    b.ir.root_block->Append(i);
+
+    auto* p = b.FunctionParam<i32>("p");
+    auto* rmw = b.Function("rmw", ty.i32());
+    rmw->SetParams({p});
+    b.Append(rmw->Block(), [&] {
+        auto* v = b.Let("v", b.Add<i32>(b.Load(i), p));
+        b.Store(i, v);
+        b.Return(rmw, v);
+    });
+
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* x = b.Name("x", b.Call(rmw, 1_i));
+        auto* if_ = b.If(true);
+        b.Append(if_->True(), [&] {  //
+            b.Return(fn, x);
+        });
+        b.Return(fn, 2_i);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %i:ptr<private, i32, read_write> = var
+}
+
+%rmw = func(%p:i32):i32 -> %b2 {
+  %b2 = block {
+    %4:i32 = load %i
+    %5:i32 = add %4, %p
+    %v:i32 = let %5
+    store %i, %v
+    ret %v
+  }
+}
+%F = func():i32 -> %b3 {
+  %b3 = block {
+    %x:i32 = call %rmw, 1i
+    if true [t: %b4] {  # if_1
+      %b4 = block {  # true
+        ret %x
+      }
+    }
+    ret 2i
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %i:ptr<private, i32, read_write> = var
+}
+
+%rmw = func(%p:i32):i32 -> %b2 {
+  %b2 = block {
+    %4:i32 = load %i
+    %5:i32 = add %4, %p
+    %v:i32 = let %5
+    store %i, %v
+    ret %v
+  }
+}
+%F = func():i32 -> %b3 {
+  %b3 = block {
+    %8:i32 = call %rmw, 1i
+    %x:i32 = let %8
+    if true [t: %b4] {  # if_1
+      %b4 = block {  # true
+        ret %x
+      }
+    }
+    ret 2i
+  }
+}
+)";
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+
+    Run(ValueToLet);  // running a second time should be no-op
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, NameMe1) {
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* v = b.Var<function, i32>("v");
+        auto* x = b.Load(v);
+        auto* y = b.Add<i32>(x, 1_i);
+        b.Store(v, 2_i);
+        b.Return(fn, y);
+    });
+
+    auto* src = R"(
+%F = func():i32 -> %b1 {
+  %b1 = block {
+    %v:ptr<function, i32, read_write> = var
+    %3:i32 = load %v
+    %4:i32 = add %3, 1i
+    store %v, 2i
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%F = func():i32 -> %b1 {
+  %b1 = block {
+    %v:ptr<function, i32, read_write> = var
+    %3:i32 = load %v
+    %4:i32 = add %3, 1i
+    %5:i32 = let %4
+    store %v, 2i
+    ret %5
+  }
+}
+)";
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+
+    Run(ValueToLet);  // running a second time should be no-op
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, NameMe2) {
+    auto* fn = b.Function("F", ty.void_());
+    b.Append(fn->Block(), [&] {
+        auto* i = b.Name("i", b.Call<i32>(core::BuiltinFn::kMax, 1_i, 2_i));
+        auto* v = b.Var<function>("v", i);
+        auto* x = b.Name("x", b.Call<i32>(core::BuiltinFn::kMax, 3_i, 4_i));
+        auto* y = b.Name("y", b.Load(v));
+        auto* z = b.Name("z", b.Add<i32>(y, x));
+        b.Store(v, z);
+        b.Return(fn);
+    });
+
+    auto* src = R"(
+%F = func():void -> %b1 {
+  %b1 = block {
+    %i:i32 = max 1i, 2i
+    %v:ptr<function, i32, read_write> = var, %i
+    %x:i32 = max 3i, 4i
+    %y:i32 = load %v
+    %z:i32 = add %y, %x
+    store %v, %z
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%F = func():void -> %b1 {
+  %b1 = block {
+    %i:i32 = max 1i, 2i
+    %v:ptr<function, i32, read_write> = var, %i
+    %x:i32 = max 3i, 4i
+    %y:i32 = load %v
+    %z:i32 = add %y, %x
+    store %v, %z
+    ret
+  }
+}
+)";
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+
+    Run(ValueToLet);  // running a second time should be no-op
+
+    EXPECT_EQ(expect, str());
+}
+}  // namespace
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/msl/writer/printer/printer.cc b/src/tint/lang/msl/writer/printer/printer.cc
index f982e92..29ccd88 100644
--- a/src/tint/lang/msl/writer/printer/printer.cc
+++ b/src/tint/lang/msl/writer/printer/printer.cc
@@ -387,7 +387,6 @@
         Switch(
             v,                                                           //
             [&](const core::ir::Constant* c) { EmitConstant(out, c); },  //
-            // [&](core::ir::FunctionParam* fp) {},                   //
             [&](const core::ir::InstructionResult* r) {
                 Switch(
                     r->Instruction(),                                        //