[ir] Add Instruction::DetachResult() helper

This removes an instruction result from an instruction and returns
it. This makes it easier to move an instruction result to a new
instruction, which should be more efficient than using
`ReplaceAllUsesWith()` to update usages one at a time.

Add overloads of a few IR Builder functions that take an
InstructionResult so that the IR builder can be used with this
pattern, instead of manually creating instructions with the allocator.

Update many transforms to use this pattern.

Change-Id: I19088816c0e715b59ea84ed1bdb092fdd2567fa6
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/186565
Commit-Queue: James Price <jrprice@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
diff --git a/src/tint/lang/core/ir/builder.h b/src/tint/lang/core/ir/builder.h
index 99e94d1..84fc0ed 100644
--- a/src/tint/lang/core/ir/builder.h
+++ b/src/tint/lang/core/ir/builder.h
@@ -960,6 +960,19 @@
     /// @returns the instruction
     ir::Discard* Discard();
 
+    /// Creates a user function call instruction with an existing instruction result
+    /// @param result the instruction result to use
+    /// @param func the function to call
+    /// @param args the call arguments
+    /// @returns the instruction
+    template <typename... ARGS>
+    ir::UserCall* CallWithResult(ir::InstructionResult* result,
+                                 ir::Function* func,
+                                 ARGS&&... args) {
+        return Append(ir.allocators.instructions.Create<ir::UserCall>(
+            result, func, Values(std::forward<ARGS>(args)...)));
+    }
+
     /// Creates a user function call instruction
     /// @param func the function to call
     /// @param args the call arguments
@@ -976,8 +989,7 @@
     /// @returns the instruction
     template <typename... ARGS>
     ir::UserCall* Call(const core::type::Type* type, ir::Function* func, ARGS&&... args) {
-        return Append(ir.allocators.instructions.Create<ir::UserCall>(
-            InstructionResult(type), func, Values(std::forward<ARGS>(args)...)));
+        return CallWithResult(InstructionResult(type), func, Values(std::forward<ARGS>(args)...));
     }
 
     /// Creates a user function call instruction
@@ -988,8 +1000,20 @@
     template <typename TYPE, typename... ARGS>
     ir::UserCall* Call(ir::Function* func, ARGS&&... args) {
         auto* type = ir.Types().Get<TYPE>();
-        return Append(ir.allocators.instructions.Create<ir::UserCall>(
-            InstructionResult(type), func, Values(std::forward<ARGS>(args)...)));
+        return CallWithResult(InstructionResult(type), func, Values(std::forward<ARGS>(args)...));
+    }
+
+    /// Creates a core builtin call instruction with an existing instruction result
+    /// @param result the instruction result to use
+    /// @param func the builtin function to call
+    /// @param args the call arguments
+    /// @returns the instruction
+    template <typename... ARGS>
+    ir::CoreBuiltinCall* CallWithResult(core::ir::InstructionResult* result,
+                                        core::BuiltinFn func,
+                                        ARGS&&... args) {
+        return Append(ir.allocators.instructions.Create<ir::CoreBuiltinCall>(
+            result, func, Values(std::forward<ARGS>(args)...)));
     }
 
     /// Creates a core builtin call instruction
@@ -999,8 +1023,7 @@
     /// @returns the instruction
     template <typename... ARGS>
     ir::CoreBuiltinCall* Call(const core::type::Type* type, core::BuiltinFn func, ARGS&&... args) {
-        return Append(ir.allocators.instructions.Create<ir::CoreBuiltinCall>(
-            InstructionResult(type), func, Values(std::forward<ARGS>(args)...)));
+        return CallWithResult(InstructionResult(type), func, Values(std::forward<ARGS>(args)...));
     }
 
     /// Creates a core builtin call instruction
@@ -1011,11 +1034,22 @@
     template <typename TYPE, typename... ARGS>
     ir::CoreBuiltinCall* Call(core::BuiltinFn func, ARGS&&... args) {
         auto* type = ir.Types().Get<TYPE>();
-        return Append(ir.allocators.instructions.Create<ir::CoreBuiltinCall>(
-            InstructionResult(type), func, Values(std::forward<ARGS>(args)...)));
+        return CallWithResult(InstructionResult(type), func, Values(std::forward<ARGS>(args)...));
     }
 
-    /// Creates a core builtin call instruction
+    /// Creates a builtin call instruction with an existing instruction result
+    /// @param result the instruction result to use
+    /// @param func the builtin function to call
+    /// @param args the call arguments
+    /// @returns the instruction
+    template <typename KLASS, typename FUNC, typename... ARGS>
+    tint::traits::EnableIf<tint::traits::IsTypeOrDerived<KLASS, ir::BuiltinCall>, KLASS*>
+    CallWithResult(ir::InstructionResult* result, FUNC func, ARGS&&... args) {
+        return Append(ir.allocators.instructions.Create<KLASS>(
+            result, func, Values(std::forward<ARGS>(args)...)));
+    }
+
+    /// Creates a builtin call instruction
     /// @param type the return type of the call
     /// @param func the builtin function to call
     /// @param args the call arguments
@@ -1023,8 +1057,8 @@
     template <typename KLASS, typename FUNC, typename... ARGS>
     tint::traits::EnableIf<tint::traits::IsTypeOrDerived<KLASS, ir::BuiltinCall>, KLASS*>
     Call(const core::type::Type* type, FUNC func, ARGS&&... args) {
-        return Append(ir.allocators.instructions.Create<KLASS>(
-            InstructionResult(type), func, Values(std::forward<ARGS>(args)...)));
+        return CallWithResult<KLASS>(InstructionResult(type), func,
+                                     Values(std::forward<ARGS>(args)...));
     }
 
     /// Creates a value conversion instruction to the template type T
@@ -1046,6 +1080,16 @@
             InstructionResult(to), Value(std::forward<VAL>(val))));
     }
 
+    /// Creates a value constructor instruction with an existing instruction result
+    /// @param result the instruction result to use
+    /// @param args the arguments to the constructor
+    /// @returns the instruction
+    template <typename... ARGS>
+    ir::Construct* ConstructWithResult(ir::InstructionResult* result, ARGS&&... args) {
+        return Append(ir.allocators.instructions.Create<ir::Construct>(
+            result, Values(std::forward<ARGS>(args)...)));
+    }
+
     /// Creates a value constructor instruction to the template type T
     /// @param args the arguments to the constructor
     /// @returns the instruction
@@ -1061,8 +1105,17 @@
     /// @returns the instruction
     template <typename... ARGS>
     ir::Construct* Construct(const core::type::Type* type, ARGS&&... args) {
-        return Append(ir.allocators.instructions.Create<ir::Construct>(
-            InstructionResult(type), Values(std::forward<ARGS>(args)...)));
+        return ConstructWithResult(InstructionResult(type), Values(std::forward<ARGS>(args)...));
+    }
+
+    /// Creates a load instruction with an existing result
+    /// @param result the instruction result to use
+    /// @param from the expression being loaded from
+    /// @returns the instruction
+    template <typename VAL>
+    ir::Load* LoadWithResult(ir::InstructionResult* result, VAL&& from) {
+        auto* value = Value(std::forward<VAL>(from));
+        return Append(ir.allocators.instructions.Create<ir::Load>(result, value));
     }
 
     /// Creates a load instruction
@@ -1071,8 +1124,7 @@
     template <typename VAL>
     ir::Load* Load(VAL&& from) {
         auto* value = Value(std::forward<VAL>(from));
-        return Append(ir.allocators.instructions.Create<ir::Load>(
-            InstructionResult(value->Type()->UnwrapPtrOrRef()), value));
+        return LoadWithResult(InstructionResult(value->Type()->UnwrapPtrOrRef()), value);
     }
 
     /// Creates a store instruction
@@ -1102,6 +1154,22 @@
                                                                                 value_val));
     }
 
+    /// Creates a load vector element instruction with an existing instruction result
+    /// @param result the instruction result to use
+    /// @param from the vector pointer expression being loaded from
+    /// @param index the new vector element index
+    /// @returns the instruction
+    template <typename FROM, typename INDEX>
+    ir::LoadVectorElement* LoadVectorElementWithResult(ir::InstructionResult* result,
+                                                       FROM&& from,
+                                                       INDEX&& index) {
+        CheckForNonDeterministicEvaluation<FROM, INDEX>();
+        auto* from_val = Value(std::forward<FROM>(from));
+        auto* index_val = Value(std::forward<INDEX>(index));
+        return Append(
+            ir.allocators.instructions.Create<ir::LoadVectorElement>(result, from_val, index_val));
+    }
+
     /// Creates a load vector element instruction
     /// @param from the vector pointer expression being loaded from
     /// @param index the new vector element index
@@ -1112,8 +1180,7 @@
         auto* from_val = Value(std::forward<FROM>(from));
         auto* index_val = Value(std::forward<INDEX>(index));
         auto* res = InstructionResult(VectorPtrElementType(from_val->Type()));
-        return Append(
-            ir.allocators.instructions.Create<ir::LoadVectorElement>(res, from_val, index_val));
+        return LoadVectorElementWithResult(res, from_val, index_val);
     }
 
     /// Creates a new `var` declaration
@@ -1351,6 +1418,19 @@
         return FunctionParam(type);
     }
 
+    /// Creates a new `Access` with an existing instruction result
+    /// @param result the instruction result to use
+    /// @param object the object being accessed
+    /// @param indices the access indices
+    /// @returns the instruction
+    template <typename OBJ, typename... ARGS>
+    ir::Access* AccessWithResult(ir::InstructionResult* result, OBJ&& object, ARGS&&... indices) {
+        CheckForNonDeterministicEvaluation<OBJ, ARGS...>();
+        auto* obj_val = Value(std::forward<OBJ>(object));
+        return Append(ir.allocators.instructions.Create<ir::Access>(
+            result, obj_val, Values(std::forward<ARGS>(indices)...)));
+    }
+
     /// Creates a new `Access`
     /// @param type the return type
     /// @param object the object being accessed
@@ -1358,10 +1438,8 @@
     /// @returns the instruction
     template <typename OBJ, typename... ARGS>
     ir::Access* Access(const core::type::Type* type, OBJ&& object, ARGS&&... indices) {
-        CheckForNonDeterministicEvaluation<OBJ, ARGS...>();
-        auto* obj_val = Value(std::forward<OBJ>(object));
-        return Append(ir.allocators.instructions.Create<ir::Access>(
-            InstructionResult(type), obj_val, Values(std::forward<ARGS>(indices)...)));
+        return AccessWithResult(InstructionResult(type), std::forward<OBJ>(object),
+                                Values(std::forward<ARGS>(indices)...));
     }
 
     /// Creates a new `Access`
diff --git a/src/tint/lang/core/ir/instruction.cc b/src/tint/lang/core/ir/instruction.cc
index 8d5218c..3f99d68 100644
--- a/src/tint/lang/core/ir/instruction.cc
+++ b/src/tint/lang/core/ir/instruction.cc
@@ -73,4 +73,11 @@
     Block()->Remove(this);
 }
 
+InstructionResult* Instruction::DetachResult() {
+    TINT_ASSERT(Results().Length() == 1u);
+    auto* result = Results()[0];
+    SetResults({});
+    return result;
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/instruction.h b/src/tint/lang/core/ir/instruction.h
index f8c72d5..cff4ab0a 100644
--- a/src/tint/lang/core/ir/instruction.h
+++ b/src/tint/lang/core/ir/instruction.h
@@ -115,6 +115,10 @@
     /// Removes this instruction from the owning block
     void Remove();
 
+    /// Detach an instruction result from this instruction.
+    /// @returns the instruction result that was detached
+    InstructionResult* DetachResult();
+
     /// @param idx the index of the operand
     /// @returns the operand with index @p idx, or `nullptr` if there are no operands or the index
     /// is out of bounds.
diff --git a/src/tint/lang/core/ir/instruction_test.cc b/src/tint/lang/core/ir/instruction_test.cc
index fa95ba5..09adeb9 100644
--- a/src/tint/lang/core/ir/instruction_test.cc
+++ b/src/tint/lang/core/ir/instruction_test.cc
@@ -30,6 +30,8 @@
 #include "src/tint/lang/core/ir/ir_helper_test.h"
 #include "src/tint/lang/core/ir/module.h"
 
+using namespace tint::core::number_suffixes;  // NOLINT
+
 namespace tint::core::ir {
 namespace {
 
@@ -166,5 +168,16 @@
         "");
 }
 
+TEST_F(IR_InstructionTest, DetachResult) {
+    auto* inst = b.Let("foo", 42_u);
+    auto* result = inst->Result(0);
+    EXPECT_EQ(result->Instruction(), inst);
+
+    auto* detached = inst->DetachResult();
+    EXPECT_EQ(detached, result);
+    EXPECT_EQ(detached->Instruction(), nullptr);
+    EXPECT_EQ(inst->Results().Length(), 0u);
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/transform/binary_polyfill.cc b/src/tint/lang/core/ir/transform/binary_polyfill.cc
index 7d4a962..a5aec3c 100644
--- a/src/tint/lang/core/ir/transform/binary_polyfill.cc
+++ b/src/tint/lang/core/ir/transform/binary_polyfill.cc
@@ -91,29 +91,18 @@
 
         // Polyfill the binary instructions that we found.
         for (auto* binary : worklist) {
-            ir::Value* replacement = nullptr;
             switch (binary->Op()) {
                 case BinaryOp::kDivide:
                 case BinaryOp::kModulo:
-                    replacement = IntDivMod(binary);
+                    IntDivMod(binary);
                     break;
                 case BinaryOp::kShiftLeft:
                 case BinaryOp::kShiftRight:
-                    replacement = MaskShiftAmount(binary);
+                    MaskShiftAmount(binary);
                     break;
                 default:
                     break;
             }
-            TINT_ASSERT(replacement);
-
-            if (replacement != binary->Result(0)) {
-                // Replace the old binary instruction result with the new value.
-                if (auto name = ir.NameOf(binary->Result(0))) {
-                    ir.SetName(replacement, name);
-                }
-                binary->Result(0)->ReplaceAllUsesWith(replacement);
-                binary->Destroy();
-            }
         }
     }
 
@@ -145,8 +134,7 @@
     /// Replace an integer divide or modulo with a call to helper function that prevents
     /// divide-by-zero and signed integer overflow.
     /// @param binary the binary instruction
-    /// @returns the replacement value
-    ir::Value* IntDivMod(ir::CoreBinary* binary) {
+    void IntDivMod(ir::CoreBinary* binary) {
         auto* result_ty = binary->Result(0)->Type();
         bool is_div = binary->Op() == BinaryOp::kDivide;
         bool is_signed = result_ty->is_signed_integer_scalar_or_vector();
@@ -217,26 +205,23 @@
         };
 
         // Call the helper function, splatting the arguments to match the target vector width.
-        Value* result = nullptr;
         b.InsertBefore(binary, [&] {
             auto* lhs = maybe_splat(binary->LHS());
             auto* rhs = maybe_splat(binary->RHS());
-            result = b.Call(result_ty, helper, lhs, rhs)->Result(0);
+            b.CallWithResult(binary->DetachResult(), helper, lhs, rhs);
         });
-        return result;
+        binary->Destroy();
     }
 
     /// Mask the RHS of a shift instruction to ensure it is modulo the bitwidth of the LHS.
     /// @param binary the binary instruction
-    /// @returns the replacement value
-    ir::Value* MaskShiftAmount(ir::CoreBinary* binary) {
+    void MaskShiftAmount(ir::CoreBinary* binary) {
         auto* lhs = binary->LHS();
         auto* rhs = binary->RHS();
         auto* mask = b.Constant(u32(lhs->Type()->DeepestElement()->Size() * 8 - 1));
         auto* masked = b.And(rhs->Type(), rhs, MatchWidth(mask, rhs->Type()));
         masked->InsertBefore(binary);
         binary->SetOperand(ir::CoreBinary::kRhsOperandOffset, masked->Result(0));
-        return binary->Result(0);
     }
 };
 
diff --git a/src/tint/lang/core/ir/transform/builtin_polyfill.cc b/src/tint/lang/core/ir/transform/builtin_polyfill.cc
index 0eb45f8..ef68d5b 100644
--- a/src/tint/lang/core/ir/transform/builtin_polyfill.cc
+++ b/src/tint/lang/core/ir/transform/builtin_polyfill.cc
@@ -147,72 +147,61 @@
 
         // Polyfill the builtin call instructions that we found.
         for (auto* builtin : worklist) {
-            ir::Value* replacement = nullptr;
             switch (builtin->Func()) {
                 case core::BuiltinFn::kClamp:
-                    replacement = ClampInt(builtin);
+                    ClampInt(builtin);
                     break;
                 case core::BuiltinFn::kCountLeadingZeros:
-                    replacement = CountLeadingZeros(builtin);
+                    CountLeadingZeros(builtin);
                     break;
                 case core::BuiltinFn::kCountTrailingZeros:
-                    replacement = CountTrailingZeros(builtin);
+                    CountTrailingZeros(builtin);
                     break;
                 case core::BuiltinFn::kExtractBits:
-                    replacement = ExtractBits(builtin);
+                    ExtractBits(builtin);
                     break;
                 case core::BuiltinFn::kFirstLeadingBit:
-                    replacement = FirstLeadingBit(builtin);
+                    FirstLeadingBit(builtin);
                     break;
                 case core::BuiltinFn::kFirstTrailingBit:
-                    replacement = FirstTrailingBit(builtin);
+                    FirstTrailingBit(builtin);
                     break;
                 case core::BuiltinFn::kInsertBits:
-                    replacement = InsertBits(builtin);
+                    InsertBits(builtin);
                     break;
                 case core::BuiltinFn::kSaturate:
-                    replacement = Saturate(builtin);
+                    Saturate(builtin);
                     break;
                 case core::BuiltinFn::kTextureSampleBaseClampToEdge:
-                    replacement = TextureSampleBaseClampToEdge_2d_f32(builtin);
+                    TextureSampleBaseClampToEdge_2d_f32(builtin);
                     break;
                 case core::BuiltinFn::kDot4I8Packed:
-                    replacement = Dot4I8Packed(builtin);
+                    Dot4I8Packed(builtin);
                     break;
                 case core::BuiltinFn::kDot4U8Packed:
-                    replacement = Dot4U8Packed(builtin);
+                    Dot4U8Packed(builtin);
                     break;
                 case core::BuiltinFn::kPack4XI8:
-                    replacement = Pack4xI8(builtin);
+                    Pack4xI8(builtin);
                     break;
                 case core::BuiltinFn::kPack4XU8:
-                    replacement = Pack4xU8(builtin);
+                    Pack4xU8(builtin);
                     break;
                 case core::BuiltinFn::kPack4XI8Clamp:
-                    replacement = Pack4xI8Clamp(builtin);
+                    Pack4xI8Clamp(builtin);
                     break;
                 case core::BuiltinFn::kPack4XU8Clamp:
-                    replacement = Pack4xU8Clamp(builtin);
+                    Pack4xU8Clamp(builtin);
                     break;
                 case core::BuiltinFn::kUnpack4XI8:
-                    replacement = Unpack4xI8(builtin);
+                    Unpack4xI8(builtin);
                     break;
                 case core::BuiltinFn::kUnpack4XU8:
-                    replacement = Unpack4xU8(builtin);
+                    Unpack4xU8(builtin);
                     break;
                 default:
                     break;
             }
-            TINT_ASSERT(replacement);
-
-            if (replacement != builtin->Result(0)) {
-                // Replace the old builtin call result with the new value.
-                if (auto name = ir.NameOf(builtin->Result(0))) {
-                    ir.SetName(replacement, name);
-                }
-                builtin->Result(0)->ReplaceAllUsesWith(replacement);
-                builtin->Destroy();
-            }
         }
     }
 
@@ -243,26 +232,22 @@
 
     /// Polyfill a `clamp()` builtin call for integers.
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* ClampInt(ir::CoreBuiltinCall* call) {
+    void ClampInt(ir::CoreBuiltinCall* call) {
         auto* type = call->Result(0)->Type();
         auto* e = call->Args()[0];
         auto* low = call->Args()[1];
         auto* high = call->Args()[2];
 
-        Value* result = nullptr;
         b.InsertBefore(call, [&] {
             auto* max = b.Call(type, core::BuiltinFn::kMax, e, low);
-            auto* min = b.Call(type, core::BuiltinFn::kMin, max, high);
-            result = min->Result(0);
+            b.CallWithResult(call->DetachResult(), core::BuiltinFn::kMin, max, high);
         });
-        return result;
+        call->Destroy();
     }
 
     /// Polyfill a `countLeadingZeros()` builtin call.
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* CountLeadingZeros(ir::CoreBuiltinCall* call) {
+    void CountLeadingZeros(ir::CoreBuiltinCall* call) {
         auto* input = call->Args()[0];
         auto* result_ty = input->Type();
         auto* uint_ty = MatchWidth(ty.u32(), result_ty);
@@ -271,7 +256,6 @@
         // Make an u32 constant with the same component count as result_ty.
         auto V = [&](uint32_t u) { return MatchWidth(b.Constant(u32(u)), result_ty); };
 
-        Value* result = nullptr;
         b.InsertBefore(call, [&] {
             // %x = %input;
             // if (%x is signed) {
@@ -309,23 +293,23 @@
                               b.LessThanEqual(bool_ty, x, V(0x7fffffff)));
             auto* b0 =
                 b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(1), b.Equal(bool_ty, x, V(0)));
-            result = b.Add(uint_ty,
-                           b.Or(uint_ty, b16,
-                                b.Or(uint_ty, b8,
-                                     b.Or(uint_ty, b4, b.Or(uint_ty, b2, b.Or(uint_ty, b1, b0))))),
-                           b0)
-                         ->Result(0);
+            Instruction* result = b.Add(
+                uint_ty,
+                b.Or(
+                    uint_ty, b16,
+                    b.Or(uint_ty, b8, b.Or(uint_ty, b4, b.Or(uint_ty, b2, b.Or(uint_ty, b1, b0))))),
+                b0);
             if (result_ty->is_signed_integer_scalar_or_vector()) {
-                result = b.Bitcast(result_ty, result)->Result(0);
+                result = b.Bitcast(result_ty, result);
             }
+            result->SetResults(Vector{call->DetachResult()});
         });
-        return result;
+        call->Destroy();
     }
 
     /// Polyfill a `countTrailingZeros()` builtin call.
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* CountTrailingZeros(ir::CoreBuiltinCall* call) {
+    void CountTrailingZeros(ir::CoreBuiltinCall* call) {
         auto* input = call->Args()[0];
         auto* result_ty = input->Type();
         auto* uint_ty = MatchWidth(ty.u32(), result_ty);
@@ -334,7 +318,6 @@
         // Make an u32 constant with the same component count as result_ty.
         auto V = [&](uint32_t u) { return MatchWidth(b.Constant(u32(u)), result_ty); };
 
-        Value* result = nullptr;
         b.InsertBefore(call, [&] {
             // %x = %input;
             // if (%x is signed) {
@@ -372,22 +355,21 @@
                               b.Equal(bool_ty, b.And(uint_ty, x, V(0x00000001)), V(0)));
             auto* b0 =
                 b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(1), b.Equal(bool_ty, x, V(0)));
-            result = b.Add(uint_ty,
-                           b.Or(uint_ty, b16,
-                                b.Or(uint_ty, b8, b.Or(uint_ty, b4, b.Or(uint_ty, b2, b1)))),
-                           b0)
-                         ->Result(0);
+            Instruction* result = b.Add(
+                uint_ty,
+                b.Or(uint_ty, b16, b.Or(uint_ty, b8, b.Or(uint_ty, b4, b.Or(uint_ty, b2, b1)))),
+                b0);
             if (result_ty->is_signed_integer_scalar_or_vector()) {
-                result = b.Bitcast(result_ty, result)->Result(0);
+                result = b.Bitcast(result_ty, result);
             }
+            result->SetResults(Vector{call->DetachResult()});
         });
-        return result;
+        call->Destroy();
     }
 
     /// Polyfill an `extractBits()` builtin call.
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* ExtractBits(ir::CoreBuiltinCall* call) {
+    void ExtractBits(ir::CoreBuiltinCall* call) {
         auto* offset = call->Args()[1];
         auto* count = call->Args()[2];
 
@@ -406,7 +388,7 @@
                     call->SetOperand(ir::CoreBuiltinCall::kArgsOperandOffset + 1, o->Result(0));
                     call->SetOperand(ir::CoreBuiltinCall::kArgsOperandOffset + 2, c->Result(0));
                 });
-                return call->Result(0);
+                break;
             }
             default:
                 TINT_UNIMPLEMENTED() << "extractBits polyfill level";
@@ -415,8 +397,7 @@
 
     /// Polyfill a `firstLeadingBit()` builtin call.
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* FirstLeadingBit(ir::CoreBuiltinCall* call) {
+    void FirstLeadingBit(ir::CoreBuiltinCall* call) {
         auto* input = call->Args()[0];
         auto* result_ty = input->Type();
         auto* uint_ty = MatchWidth(ty.u32(), result_ty);
@@ -425,7 +406,6 @@
         // Make an u32 constant with the same component count as result_ty.
         auto V = [&](uint32_t u) { return MatchWidth(b.Constant(u32(u)), result_ty); };
 
-        Value* result = nullptr;
         b.InsertBefore(call, [&] {
             // %x = %input;
             // if (%x is signed) {
@@ -465,22 +445,21 @@
             x = b.ShiftRight(uint_ty, x, b2)->Result(0);
             auto* b1 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(1), V(0),
                               b.Equal(bool_ty, b.And(uint_ty, x, V(0x00000002)), V(0)));
-            result = b.Or(uint_ty, b16, b.Or(uint_ty, b8, b.Or(uint_ty, b4, b.Or(uint_ty, b2, b1))))
-                         ->Result(0);
+            Instruction* result =
+                b.Or(uint_ty, b16, b.Or(uint_ty, b8, b.Or(uint_ty, b4, b.Or(uint_ty, b2, b1))));
             result = b.Call(uint_ty, core::BuiltinFn::kSelect, result, V(0xffffffff),
-                            b.Equal(bool_ty, x, V(0)))
-                         ->Result(0);
+                            b.Equal(bool_ty, x, V(0)));
             if (result_ty->is_signed_integer_scalar_or_vector()) {
-                result = b.Bitcast(result_ty, result)->Result(0);
+                result = b.Bitcast(result_ty, result);
             }
+            result->SetResults(Vector{call->DetachResult()});
         });
-        return result;
+        call->Destroy();
     }
 
     /// Polyfill a `firstTrailingBit()` builtin call.
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* FirstTrailingBit(ir::CoreBuiltinCall* call) {
+    void FirstTrailingBit(ir::CoreBuiltinCall* call) {
         auto* input = call->Args()[0];
         auto* result_ty = input->Type();
         auto* uint_ty = MatchWidth(ty.u32(), result_ty);
@@ -489,7 +468,6 @@
         // Make an u32 constant with the same component count as result_ty.
         auto V = [&](uint32_t u) { return MatchWidth(b.Constant(u32(u)), result_ty); };
 
-        Value* result = nullptr;
         b.InsertBefore(call, [&] {
             // %x = %input;
             // if (%x is signed) {
@@ -525,22 +503,21 @@
             x = b.ShiftRight(uint_ty, x, b2)->Result(0);
             auto* b1 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(1),
                               b.Equal(bool_ty, b.And(uint_ty, x, V(0x00000001)), V(0)));
-            result = b.Or(uint_ty, b16, b.Or(uint_ty, b8, b.Or(uint_ty, b4, b.Or(uint_ty, b2, b1))))
-                         ->Result(0);
+            Instruction* result =
+                b.Or(uint_ty, b16, b.Or(uint_ty, b8, b.Or(uint_ty, b4, b.Or(uint_ty, b2, b1))));
             result = b.Call(uint_ty, core::BuiltinFn::kSelect, result, V(0xffffffff),
-                            b.Equal(bool_ty, x, V(0)))
-                         ->Result(0);
+                            b.Equal(bool_ty, x, V(0)));
             if (result_ty->is_signed_integer_scalar_or_vector()) {
-                result = b.Bitcast(result_ty, result)->Result(0);
+                result = b.Bitcast(result_ty, result);
             }
+            result->SetResults(Vector{call->DetachResult()});
         });
-        return result;
+        call->Destroy();
     }
 
     /// Polyfill an `insertBits()` builtin call.
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* InsertBits(ir::CoreBuiltinCall* call) {
+    void InsertBits(ir::CoreBuiltinCall* call) {
         auto* offset = call->Args()[2];
         auto* count = call->Args()[3];
 
@@ -559,7 +536,7 @@
                     call->SetOperand(ir::CoreBuiltinCall::kArgsOperandOffset + 2, o->Result(0));
                     call->SetOperand(ir::CoreBuiltinCall::kArgsOperandOffset + 3, c->Result(0));
                 });
-                return call->Result(0);
+                break;
             }
             default:
                 TINT_UNIMPLEMENTED() << "insertBits polyfill level";
@@ -568,8 +545,7 @@
 
     /// Polyfill a `saturate()` builtin call.
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* Saturate(ir::CoreBuiltinCall* call) {
+    void Saturate(ir::CoreBuiltinCall* call) {
         // Replace `saturate(x)` with `clamp(x, 0., 1.)`.
         auto* type = call->Result(0)->Type();
         ir::Constant* zero = nullptr;
@@ -581,21 +557,20 @@
             zero = MatchWidth(b.Constant(0_h), type);
             one = MatchWidth(b.Constant(1_h), type);
         }
-        auto* clamp = b.Call(type, core::BuiltinFn::kClamp, Vector{call->Args()[0], zero, one});
+        auto* clamp = b.CallWithResult(call->DetachResult(), core::BuiltinFn::kClamp,
+                                       Vector{call->Args()[0], zero, one});
         clamp->InsertBefore(call);
-        return clamp->Result(0);
+        call->Destroy();
     }
 
     /// Polyfill a `textureSampleBaseClampToEdge()` builtin call for 2D F32 textures.
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* TextureSampleBaseClampToEdge_2d_f32(ir::CoreBuiltinCall* call) {
+    void TextureSampleBaseClampToEdge_2d_f32(ir::CoreBuiltinCall* call) {
         // Replace `textureSampleBaseClampToEdge(%texture, %sample, %coords)` with:
         //   %dims       = vec2f(textureDimensions(%texture));
         //   %half_texel = vec2f(0.5) / dims;
         //   %clamped    = clamp(%coord, %half_texel, 1.0 - %half_texel);
         //   %result     = textureSampleLevel(%texture, %sampler, %clamped, 0);
-        ir::Value* result = nullptr;
         auto* texture = call->Args()[0];
         auto* sampler = call->Args()[1];
         auto* coords = call->Args()[2];
@@ -607,17 +582,15 @@
             auto* one_minus_half_texel = b.Subtract(vec2f, b.Splat(vec2f, 1_f, 2), half_texel);
             auto* clamped =
                 b.Call(vec2f, core::BuiltinFn::kClamp, coords, half_texel, one_minus_half_texel);
-            result = b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureSampleLevel, texture, sampler,
-                            clamped, 0_f)
-                         ->Result(0);
+            b.CallWithResult(call->DetachResult(), core::BuiltinFn::kTextureSampleLevel, texture,
+                             sampler, clamped, 0_f);
         });
-        return result;
+        call->Destroy();
     }
 
     /// Polyfill a `dot4I8Packed()` builtin call
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* Dot4I8Packed(ir::CoreBuiltinCall* call) {
+    void Dot4I8Packed(ir::CoreBuiltinCall* call) {
         // Replace `dot4I8Packed(%x,%y)` with:
         //   %unpacked_x = unpack4xI8(%x);
         //   %unpacked_y = unpack4xI8(%y);
@@ -626,17 +599,15 @@
         auto* y = call->Args()[1];
         auto* unpacked_x = Unpack4xI8OnValue(call, x);
         auto* unpacked_y = Unpack4xI8OnValue(call, y);
-        ir::Value* result = nullptr;
         b.InsertBefore(call, [&] {
-            result = b.Call(ty.i32(), core::BuiltinFn::kDot, unpacked_x, unpacked_y)->Result(0);
+            b.CallWithResult(call->DetachResult(), core::BuiltinFn::kDot, unpacked_x, unpacked_y);
         });
-        return result;
+        call->Destroy();
     }
 
     /// Polyfill a `dot4U8Packed()` builtin call
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* Dot4U8Packed(ir::CoreBuiltinCall* call) {
+    void Dot4U8Packed(ir::CoreBuiltinCall* call) {
         // Replace `dot4U8Packed(%x,%y)` with:
         //   %unpacked_x = unpack4xU8(%x);
         //   %unpacked_y = unpack4xU8(%y);
@@ -645,23 +616,20 @@
         auto* y = call->Args()[1];
         auto* unpacked_x = Unpack4xU8OnValue(call, x);
         auto* unpacked_y = Unpack4xU8OnValue(call, y);
-        ir::Value* result = nullptr;
         b.InsertBefore(call, [&] {
-            result = b.Call(ty.u32(), core::BuiltinFn::kDot, unpacked_x, unpacked_y)->Result(0);
+            b.CallWithResult(call->DetachResult(), core::BuiltinFn::kDot, unpacked_x, unpacked_y);
         });
-        return result;
+        call->Destroy();
     }
 
     /// Polyfill a `pack4xI8()` builtin call
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* Pack4xI8(ir::CoreBuiltinCall* call) {
+    void Pack4xI8(ir::CoreBuiltinCall* call) {
         // Replace `pack4xI8(%x)` with:
         //   %n      = vec4u(0, 8, 16, 24);
         //   %x_u32  = bitcast<vec4u>(%x)
         //   %x_u8   = (%x_u32 & vec4u(0xff)) << n;
         //   %result = dot(%x_u8, vec4u(1));
-        ir::Value* result = nullptr;
         auto* x = call->Args()[0];
         b.InsertBefore(call, [&] {
             auto* vec4u = ty.vec4<u32>();
@@ -671,22 +639,19 @@
             auto* x_u32 = b.Bitcast(vec4u, x);
             auto* x_u8 = b.ShiftLeft(
                 vec4u, b.And(vec4u, x_u32, b.Construct(vec4u, b.Constant(u32(0xff)))), n);
-            result = b.Call(ty.u32(), core::BuiltinFn::kDot, x_u8,
-                            b.Construct(vec4u, (b.Constant(u32(1)))))
-                         ->Result(0);
+            b.CallWithResult(call->DetachResult(), core::BuiltinFn::kDot, x_u8,
+                             b.Construct(vec4u, (b.Constant(u32(1)))));
         });
-        return result;
+        call->Destroy();
     }
 
     /// Polyfill a `pack4xU8()` builtin call
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* Pack4xU8(ir::CoreBuiltinCall* call) {
+    void Pack4xU8(ir::CoreBuiltinCall* call) {
         // Replace `pack4xU8(%x)` with:
         //   %n      = vec4u(0, 8, 16, 24);
         //   %x_i8   = (%x & vec4u(0xff)) << %n;
         //   %result = dot(%x_i8, vec4u(1));
-        ir::Value* result = nullptr;
         auto* x = call->Args()[0];
         b.InsertBefore(call, [&] {
             auto* vec4u = ty.vec4<u32>();
@@ -695,17 +660,15 @@
                                   b.Constant(u32(16)), b.Constant(u32(24)));
             auto* x_u8 =
                 b.ShiftLeft(vec4u, b.And(vec4u, x, b.Construct(vec4u, b.Constant(u32(0xff)))), n);
-            result = b.Call(ty.u32(), core::BuiltinFn::kDot, x_u8,
-                            b.Construct(vec4u, (b.Constant(u32(1)))))
-                         ->Result(0);
+            b.CallWithResult(call->DetachResult(), core::BuiltinFn::kDot, x_u8,
+                             b.Construct(vec4u, (b.Constant(u32(1)))));
         });
-        return result;
+        call->Destroy();
     }
 
     /// Polyfill a `pack4xI8Clamp()` builtin call
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* Pack4xI8Clamp(ir::CoreBuiltinCall* call) {
+    void Pack4xI8Clamp(ir::CoreBuiltinCall* call) {
         // Replace `pack4xI8Clamp(%x)` with:
         //   %n           = vec4u(0, 8, 16, 24);
         //   %min_i8_vec4 = vec4i(-128);
@@ -714,7 +677,6 @@
         //   %x_u32       = bitcast<vec4u>(%x_clamp);
         //   %x_u8        = (%x_u32 & vec4u(0xff)) << n;
         //   %result      = dot(%x_u8, vec4u(1));
-        ir::Value* result = nullptr;
         auto* x = call->Args()[0];
         b.InsertBefore(call, [&] {
             auto* vec4i = ty.vec4<i32>();
@@ -728,17 +690,15 @@
             auto* x_u32 = b.Bitcast(vec4u, x_clamp);
             auto* x_u8 = b.ShiftLeft(
                 vec4u, b.And(vec4u, x_u32, b.Construct(vec4u, b.Constant(u32(0xff)))), n);
-            result = b.Call(ty.u32(), core::BuiltinFn::kDot, x_u8,
-                            b.Construct(vec4u, (b.Constant(u32(1)))))
-                         ->Result(0);
+            b.CallWithResult(call->DetachResult(), core::BuiltinFn::kDot, x_u8,
+                             b.Construct(vec4u, (b.Constant(u32(1)))));
         });
-        return result;
+        call->Destroy();
     }
 
     /// Polyfill a `pack4xU8Clamp()` builtin call
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* Pack4xU8Clamp(ir::CoreBuiltinCall* call) {
+    void Pack4xU8Clamp(ir::CoreBuiltinCall* call) {
         // Replace `pack4xU8Clamp(%x)` with:
         //   %n       = vec4u(0, 8, 16, 24);
         //   %min_u8_vec4 = vec4u(0);
@@ -746,7 +706,6 @@
         //   %x_clamp = clamp(%x, vec4u(0), vec4u(255));
         //   %x_u8    = %x_clamp << n;
         //   %result  = dot(%x_u8, vec4u(1));
-        ir::Value* result = nullptr;
         auto* x = call->Args()[0];
         b.InsertBefore(call, [&] {
             auto* vec4u = ty.vec4<u32>();
@@ -757,23 +716,22 @@
             auto* max_u8_vec4 = b.Construct(vec4u, b.Constant(u32(255)));
             auto* x_clamp = b.Call(vec4u, core::BuiltinFn::kClamp, x, min_u8_vec4, max_u8_vec4);
             auto* x_u8 = b.ShiftLeft(vec4u, x_clamp, n);
-            result = b.Call(ty.u32(), core::BuiltinFn::kDot, x_u8,
-                            b.Construct(vec4u, (b.Constant(u32(1)))))
-                         ->Result(0);
+            b.CallWithResult(call->DetachResult(), core::BuiltinFn::kDot, x_u8,
+                             b.Construct(vec4u, (b.Constant(u32(1)))));
         });
-        return result;
+        call->Destroy();
     }
 
     /// Emit code for `unpack4xI8` on u32 value `x`, before the given call.
     /// @param call the instruction that should follow the emitted code
     /// @param x the u32 value to be unpacked
-    ir::Value* Unpack4xI8OnValue(ir::CoreBuiltinCall* call, ir::Value* x) {
+    ir::Instruction* Unpack4xI8OnValue(ir::CoreBuiltinCall* call, ir::Value* x) {
         // Replace `unpack4xI8(%x)` with:
         //   %n       = vec4u(24, 16, 8, 0);
         //   %x_splat = vec4u(%x); // splat the scalar to a vector
         //   %x_vec4i = bitcast<vec4i>(%x_splat << n);
         //   %result  = %x_vec4i >> vec4u(24);
-        ir::Value* result = nullptr;
+        ir::Instruction* result = nullptr;
         b.InsertBefore(call, [&] {
             auto* vec4i = ty.vec4<i32>();
             auto* vec4u = ty.vec4<u32>();
@@ -782,29 +740,29 @@
                                   b.Constant(u32(8)), b.Constant(u32(0)));
             auto* x_splat = b.Construct(vec4u, x);
             auto* x_vec4i = b.Bitcast(vec4i, b.ShiftLeft(vec4u, x_splat, n));
-            result =
-                b.ShiftRight(vec4i, x_vec4i, b.Construct(vec4u, b.Constant(u32(24))))->Result(0);
+            result = b.ShiftRight(vec4i, x_vec4i, b.Construct(vec4u, b.Constant(u32(24))));
         });
         return result;
     }
 
     /// Polyfill a `unpack4xI8()` builtin call
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* Unpack4xI8(ir::CoreBuiltinCall* call) {
-        return Unpack4xI8OnValue(call, call->Args()[0]);
+    void Unpack4xI8(ir::CoreBuiltinCall* call) {
+        auto* result = Unpack4xI8OnValue(call, call->Args()[0]);
+        result->SetResults(Vector{call->DetachResult()});
+        call->Destroy();
     }
 
     /// Emit code for `unpack4xU8` on u32 value `x`, before the given call.
     /// @param call the instruction that should follow the emitted code
     /// @param x the u32 value to be unpacked
-    ir::Value* Unpack4xU8OnValue(ir::CoreBuiltinCall* call, ir::Value* x) {
+    Instruction* Unpack4xU8OnValue(ir::CoreBuiltinCall* call, ir::Value* x) {
         // Replace `unpack4xU8(%x)` with:
         //   %n       = vec4u(0, 8, 16, 24);
         //   %x_splat = vec4u(%x); // splat the scalar to a vector
         //   %x_vec4u = %x_splat >> n;
         //   %result  = %x_vec4u & vec4u(0xff);
-        ir::Value* result = nullptr;
+        ir::Instruction* result = nullptr;
         b.InsertBefore(call, [&] {
             auto* vec4u = ty.vec4<u32>();
 
@@ -812,16 +770,17 @@
                                   b.Constant(u32(16)), b.Constant(u32(24)));
             auto* x_splat = b.Construct(vec4u, x);
             auto* x_vec4u = b.ShiftRight(vec4u, x_splat, n);
-            result = b.And(vec4u, x_vec4u, b.Construct(vec4u, b.Constant(u32(0xff))))->Result(0);
+            result = b.And(vec4u, x_vec4u, b.Construct(vec4u, b.Constant(u32(0xff))));
         });
         return result;
     }
 
     /// Polyfill a `unpack4xU8()` builtin call
     /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* Unpack4xU8(ir::CoreBuiltinCall* call) {
-        return Unpack4xU8OnValue(call, call->Args()[0]);
+    void Unpack4xU8(ir::CoreBuiltinCall* call) {
+        auto* result = Unpack4xU8OnValue(call, call->Args()[0]);
+        result->SetResults(Vector{call->DetachResult()});
+        call->Destroy();
     }
 };
 
diff --git a/src/tint/lang/core/ir/transform/conversion_polyfill.cc b/src/tint/lang/core/ir/transform/conversion_polyfill.cc
index c5f7bbd..76d1920 100644
--- a/src/tint/lang/core/ir/transform/conversion_polyfill.cc
+++ b/src/tint/lang/core/ir/transform/conversion_polyfill.cc
@@ -82,13 +82,7 @@
 
         // Polyfill the conversion instructions that we found.
         for (auto* convert : ftoi_worklist) {
-            auto* replacement = ftoi(convert);
-
-            // Replace the old conversion instruction result with the new value.
-            if (auto name = ir.NameOf(convert->Result(0))) {
-                ir.SetName(replacement, name);
-            }
-            convert->Result(0)->ReplaceAllUsesWith(replacement);
+            ftoi(convert);
             convert->Destroy();
         }
     }
@@ -96,8 +90,7 @@
     /// Replace a conversion instruction with a call to helper function that manually clamps the
     /// result to within the limit of the destination type.
     /// @param convert the conversion instruction
-    /// @returns the replacement value
-    ir::Value* ftoi(ir::Convert* convert) {
+    void ftoi(ir::Convert* convert) {
         auto* res_ty = convert->Result(0)->Type();
         auto* src_ty = convert->Args()[0]->Type();
         auto* src_el_ty = src_ty->DeepestElement();
@@ -190,9 +183,8 @@
         });
 
         // Call the helper function, splatting the arguments to match the target vector width.
-        auto* call = b.Call(res_ty, helper, convert->Args()[0]);
+        auto* call = b.CallWithResult(convert->DetachResult(), helper, convert->Args()[0]);
         call->InsertBefore(convert);
-        return call->Result(0);
     }
 
     /// Return a type with element type @p type that has the same number of vector components as
diff --git a/src/tint/lang/core/ir/transform/multiplanar_external_texture.cc b/src/tint/lang/core/ir/transform/multiplanar_external_texture.cc
index d63e3ba..aa0d1dc 100644
--- a/src/tint/lang/core/ir/transform/multiplanar_external_texture.cc
+++ b/src/tint/lang/core/ir/transform/multiplanar_external_texture.cc
@@ -240,19 +240,18 @@
                         }
 
                         // Call the `TextureLoadExternal()` helper function.
-                        auto* helper = b.Call(ty.vec4<f32>(), TextureLoadExternal(), plane_0,
-                                              plane_1, params, coords);
+                        auto* helper = b.CallWithResult(call->DetachResult(), TextureLoadExternal(),
+                                                        plane_0, plane_1, params, coords);
                         helper->InsertBefore(call);
-                        call->Result(0)->ReplaceAllUsesWith(helper->Result(0));
                         call->Destroy();
                     } else if (call->Func() == core::BuiltinFn::kTextureSampleBaseClampToEdge) {
                         // Call the `TextureSampleExternal()` helper function.
                         auto* sampler = call->Args()[1];
                         auto* coords = call->Args()[2];
-                        auto* helper = b.Call(ty.vec4<f32>(), TextureSampleExternal(), plane_0,
-                                              plane_1, params, sampler, coords);
+                        auto* helper =
+                            b.CallWithResult(call->DetachResult(), TextureSampleExternal(), plane_0,
+                                             plane_1, params, sampler, coords);
                         helper->InsertBefore(call);
-                        call->Result(0)->ReplaceAllUsesWith(helper->Result(0));
                         call->Destroy();
                     } else {
                         TINT_ICE() << "unhandled texture_external builtin call: " << call->Func();
diff --git a/src/tint/lang/core/ir/transform/multiplanar_external_texture_test.cc b/src/tint/lang/core/ir/transform/multiplanar_external_texture_test.cc
index 6597fb8..f032ba8 100644
--- a/src/tint/lang/core/ir/transform/multiplanar_external_texture_test.cc
+++ b/src/tint/lang/core/ir/transform/multiplanar_external_texture_test.cc
@@ -352,8 +352,8 @@
     %6:texture_2d<f32> = load %texture_plane0
     %7:texture_2d<f32> = load %texture_plane1
     %8:tint_ExternalTextureParams = load %texture_params
-    %9:vec4<f32> = call %tint_TextureLoadExternal, %6, %7, %8, %coords
-    ret %9
+    %result:vec4<f32> = call %tint_TextureLoadExternal, %6, %7, %8, %coords
+    ret %result
   }
 }
 %tint_TextureLoadExternal = func(%plane_0:texture_2d<f32>, %plane_1:texture_2d<f32>, %params:tint_ExternalTextureParams, %coords_1:vec2<u32>):vec4<f32> {  # %coords_1: 'coords'
@@ -514,8 +514,8 @@
     %7:texture_2d<f32> = load %texture_plane1
     %8:tint_ExternalTextureParams = load %texture_params
     %9:vec2<u32> = convert %coords
-    %10:vec4<f32> = call %tint_TextureLoadExternal, %6, %7, %8, %9
-    ret %10
+    %result:vec4<f32> = call %tint_TextureLoadExternal, %6, %7, %8, %9
+    ret %result
   }
 }
 %tint_TextureLoadExternal = func(%plane_0:texture_2d<f32>, %plane_1:texture_2d<f32>, %params:tint_ExternalTextureParams, %coords_1:vec2<u32>):vec4<f32> {  # %coords_1: 'coords'
@@ -677,8 +677,8 @@
     %7:texture_2d<f32> = load %texture_plane0
     %8:texture_2d<f32> = load %texture_plane1
     %9:tint_ExternalTextureParams = load %texture_params
-    %10:vec4<f32> = call %tint_TextureSampleExternal, %7, %8, %9, %sampler, %coords
-    ret %10
+    %result:vec4<f32> = call %tint_TextureSampleExternal, %7, %8, %9, %sampler, %coords
+    ret %result
   }
 }
 %tint_TextureSampleExternal = func(%plane_0:texture_2d<f32>, %plane_1:texture_2d<f32>, %params:tint_ExternalTextureParams, %sampler_1:sampler, %coords_1:vec2<f32>):vec4<f32> {  # %sampler_1: 'sampler', %coords_1: 'coords'
@@ -856,8 +856,8 @@
 
 %foo = func(%texture_plane0_1:texture_2d<f32>, %texture_plane1_1:texture_2d<f32>, %texture_params_1:tint_ExternalTextureParams, %sampler:sampler, %coords:vec2<f32>):vec4<f32> {  # %texture_plane0_1: 'texture_plane0', %texture_plane1_1: 'texture_plane1', %texture_params_1: 'texture_params'
   $B2: {
-    %10:vec4<f32> = call %tint_TextureSampleExternal, %texture_plane0_1, %texture_plane1_1, %texture_params_1, %sampler, %coords
-    ret %10
+    %result:vec4<f32> = call %tint_TextureSampleExternal, %texture_plane0_1, %texture_plane1_1, %texture_params_1, %sampler, %coords
+    ret %result
   }
 }
 %bar = func(%sampler_1:sampler, %coords_1:vec2<f32>):vec4<f32> {  # %sampler_1: 'sampler', %coords_1: 'coords'
@@ -865,8 +865,8 @@
     %15:texture_2d<f32> = load %texture_plane0
     %16:texture_2d<f32> = load %texture_plane1
     %17:tint_ExternalTextureParams = load %texture_params
-    %result:vec4<f32> = call %foo, %15, %16, %17, %sampler_1, %coords_1
-    ret %result
+    %result_1:vec4<f32> = call %foo, %15, %16, %17, %sampler_1, %coords_1  # %result_1: 'result'
+    ret %result_1
   }
 }
 %tint_TextureSampleExternal = func(%plane_0:texture_2d<f32>, %plane_1:texture_2d<f32>, %params:tint_ExternalTextureParams, %sampler_2:sampler, %coords_2:vec2<f32>):vec4<f32> {  # %sampler_2: 'sampler', %coords_2: 'coords'
@@ -1062,8 +1062,8 @@
 
 %foo = func(%texture_plane0_1:texture_2d<f32>, %texture_plane1_1:texture_2d<f32>, %texture_params_1:tint_ExternalTextureParams, %sampler:sampler, %coords:vec2<f32>):vec4<f32> {  # %texture_plane0_1: 'texture_plane0', %texture_plane1_1: 'texture_plane1', %texture_params_1: 'texture_params'
   $B2: {
-    %10:vec4<f32> = call %tint_TextureSampleExternal, %texture_plane0_1, %texture_plane1_1, %texture_params_1, %sampler, %coords
-    ret %10
+    %result:vec4<f32> = call %tint_TextureSampleExternal, %texture_plane0_1, %texture_plane1_1, %texture_params_1, %sampler, %coords
+    ret %result
   }
 }
 %bar = func(%sampler_1:sampler, %coords_1:vec2<f32>):vec4<f32> {  # %sampler_1: 'sampler', %coords_1: 'coords'
diff --git a/src/tint/lang/core/ir/transform/std140.cc b/src/tint/lang/core/ir/transform/std140.cc
index f2caaf5..71b02ce 100644
--- a/src/tint/lang/core/ir/transform/std140.cc
+++ b/src/tint/lang/core/ir/transform/std140.cc
@@ -333,9 +333,7 @@
                     if (!replacement->Type()->Is<core::type::Pointer>()) {
                         // We have loaded a decomposed matrix and reconstructed it, so this is now
                         // extracting from a value type.
-                        auto* access =
-                            b.Access(load->Result(0)->Type(), replacement, load->Index());
-                        load->Result(0)->ReplaceAllUsesWith(access->Result(0));
+                        b.AccessWithResult(load->DetachResult(), replacement, load->Index());
                         load->Destroy();
                     } else {
                         // There was no decomposed matrix on the path to this instruction so just
diff --git a/src/tint/lang/core/ir/transform/vectorize_scalar_matrix_constructors.cc b/src/tint/lang/core/ir/transform/vectorize_scalar_matrix_constructors.cc
index 37bdb03..99d6be9 100644
--- a/src/tint/lang/core/ir/transform/vectorize_scalar_matrix_constructors.cc
+++ b/src/tint/lang/core/ir/transform/vectorize_scalar_matrix_constructors.cc
@@ -86,8 +86,7 @@
         }
 
         // Construct the matrix from the column vectors and replace the original instruction.
-        auto* replacement = b.Construct(mat, std::move(columns))->Result(0);
-        construct->Result(0)->ReplaceAllUsesWith(replacement);
+        b.ConstructWithResult(construct->DetachResult(), std::move(columns));
         construct->Destroy();
     }
 };
diff --git a/src/tint/lang/msl/writer/raise/builtin_polyfill.cc b/src/tint/lang/msl/writer/raise/builtin_polyfill.cc
index e41330f..e44d1a6 100644
--- a/src/tint/lang/msl/writer/raise/builtin_polyfill.cc
+++ b/src/tint/lang/msl/writer/raise/builtin_polyfill.cc
@@ -73,42 +73,32 @@
 
         // Replace the builtins that we found.
         for (auto* builtin : worklist) {
-            core::ir::Value* replacement = nullptr;
             switch (builtin->Func()) {
                 case core::BuiltinFn::kStorageBarrier:
-                    replacement = ThreadgroupBarrier(builtin, BarrierType::kDevice);
+                    ThreadgroupBarrier(builtin, BarrierType::kDevice);
                     break;
                 case core::BuiltinFn::kWorkgroupBarrier:
-                    replacement = ThreadgroupBarrier(builtin, BarrierType::kThreadGroup);
+                    ThreadgroupBarrier(builtin, BarrierType::kThreadGroup);
                     break;
                 case core::BuiltinFn::kTextureBarrier:
-                    replacement = ThreadgroupBarrier(builtin, BarrierType::kTexture);
+                    ThreadgroupBarrier(builtin, BarrierType::kTexture);
                     break;
                 default:
                     break;
             }
-            TINT_ASSERT(replacement);
-
-            // Replace the old builtin result with the new value.
-            if (auto name = ir.NameOf(builtin->Result(0))) {
-                ir.SetName(replacement, name);
-            }
-            builtin->Result(0)->ReplaceAllUsesWith(replacement);
-            builtin->Destroy();
         }
     }
 
     /// Replace a barrier builtin with the `threadgroupBarrier()` intrinsic.
     /// @param builtin the builtin call instruction
     /// @param type the barrier type
-    /// @returns the replacement value
-    core::ir::Value* ThreadgroupBarrier(core::ir::CoreBuiltinCall* builtin, BarrierType type) {
+    void ThreadgroupBarrier(core::ir::CoreBuiltinCall* builtin, BarrierType type) {
         // Replace the builtin call with a call to the msl.threadgroup_barrier intrinsic.
         auto args = Vector<core::ir::Value*, 1>{b.Constant(u32(type))};
-        auto* call = b.Call<msl::ir::BuiltinCall>(
-            builtin->Result(0)->Type(), msl::BuiltinFn::kThreadgroupBarrier, std::move(args));
+        auto* call = b.CallWithResult<msl::ir::BuiltinCall>(
+            builtin->DetachResult(), msl::BuiltinFn::kThreadgroupBarrier, std::move(args));
         call->InsertBefore(builtin);
-        return call->Result(0);
+        builtin->Destroy();
     }
 };
 
diff --git a/src/tint/lang/spirv/reader/lower/vector_element_pointer.cc b/src/tint/lang/spirv/reader/lower/vector_element_pointer.cc
index daf59df..948104d 100644
--- a/src/tint/lang/spirv/reader/lower/vector_element_pointer.cc
+++ b/src/tint/lang/spirv/reader/lower/vector_element_pointer.cc
@@ -131,9 +131,8 @@
             Switch(
                 use.instruction,
                 [&](core::ir::Load* load) {
-                    auto* lve = b.LoadVectorElement(object, index);
+                    auto* lve = b.LoadVectorElementWithResult(load->DetachResult(), object, index);
                     lve->InsertBefore(load);
-                    load->Result(0)->ReplaceAllUsesWith(lve->Result(0));
                     to_destroy.Push(load);
                 },
                 [&](core::ir::Store* store) {
diff --git a/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc b/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
index b45beac..70808f6 100644
--- a/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
+++ b/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
@@ -114,10 +114,9 @@
 
         // Replace the builtins that we found.
         for (auto* builtin : worklist) {
-            core::ir::Value* replacement = nullptr;
             switch (builtin->Func()) {
                 case core::BuiltinFn::kArrayLength:
-                    replacement = ArrayLength(builtin);
+                    ArrayLength(builtin);
                     break;
                 case core::BuiltinFn::kAtomicAdd:
                 case core::BuiltinFn::kAtomicAnd:
@@ -130,30 +129,30 @@
                 case core::BuiltinFn::kAtomicStore:
                 case core::BuiltinFn::kAtomicSub:
                 case core::BuiltinFn::kAtomicXor:
-                    replacement = Atomic(builtin);
+                    Atomic(builtin);
                     break;
                 case core::BuiltinFn::kDot:
-                    replacement = Dot(builtin);
+                    Dot(builtin);
                     break;
                 case core::BuiltinFn::kDot4I8Packed:
                 case core::BuiltinFn::kDot4U8Packed:
-                    replacement = DotPacked4x8(builtin);
+                    DotPacked4x8(builtin);
                     break;
                 case core::BuiltinFn::kSelect:
-                    replacement = Select(builtin);
+                    Select(builtin);
                     break;
                 case core::BuiltinFn::kTextureDimensions:
-                    replacement = TextureDimensions(builtin);
+                    TextureDimensions(builtin);
                     break;
                 case core::BuiltinFn::kTextureGather:
                 case core::BuiltinFn::kTextureGatherCompare:
-                    replacement = TextureGather(builtin);
+                    TextureGather(builtin);
                     break;
                 case core::BuiltinFn::kTextureLoad:
-                    replacement = TextureLoad(builtin);
+                    TextureLoad(builtin);
                     break;
                 case core::BuiltinFn::kTextureNumLayers:
-                    replacement = TextureNumLayers(builtin);
+                    TextureNumLayers(builtin);
                     break;
                 case core::BuiltinFn::kTextureSample:
                 case core::BuiltinFn::kTextureSampleBias:
@@ -161,25 +160,17 @@
                 case core::BuiltinFn::kTextureSampleCompareLevel:
                 case core::BuiltinFn::kTextureSampleGrad:
                 case core::BuiltinFn::kTextureSampleLevel:
-                    replacement = TextureSample(builtin);
+                    TextureSample(builtin);
                     break;
                 case core::BuiltinFn::kTextureStore:
-                    replacement = TextureStore(builtin);
+                    TextureStore(builtin);
                     break;
                 case core::BuiltinFn::kQuantizeToF16:
-                    replacement = QuantizeToF16Vec(builtin);
+                    QuantizeToF16Vec(builtin);
                     break;
                 default:
                     break;
             }
-            TINT_ASSERT(replacement);
-
-            // Replace the old builtin result with the new value.
-            if (auto name = ir.NameOf(builtin->Result(0))) {
-                ir.SetName(replacement, name);
-            }
-            builtin->Result(0)->ReplaceAllUsesWith(replacement);
-            builtin->Destroy();
         }
     }
 
@@ -192,8 +183,7 @@
 
     /// Handle an `arrayLength()` builtin.
     /// @param builtin the builtin call instruction
-    /// @returns the replacement value
-    core::ir::Value* ArrayLength(core::ir::CoreBuiltinCall* builtin) {
+    void ArrayLength(core::ir::CoreBuiltinCall* builtin) {
         // Strip away any let instructions to get to the original struct member access instruction.
         auto* ptr = builtin->Args()[0]->As<core::ir::InstructionResult>();
         while (auto* let = tint::As<core::ir::Let>(ptr->Instruction())) {
@@ -208,17 +198,16 @@
         auto* const_idx = access->Indices()[0]->As<core::ir::Constant>();
 
         // Replace the builtin call with a call to the spirv.array_length intrinsic.
-        auto* call = b.Call<spirv::ir::BuiltinCall>(
-            builtin->Result(0)->Type(), spirv::BuiltinFn::kArrayLength,
+        auto* call = b.CallWithResult<spirv::ir::BuiltinCall>(
+            builtin->DetachResult(), spirv::BuiltinFn::kArrayLength,
             Vector{access->Object(), Literal(u32(const_idx->Value()->ValueAs<uint32_t>()))});
         call->InsertBefore(builtin);
-        return call->Result(0);
+        builtin->Destroy();
     }
 
     /// Handle an atomic*() builtin.
     /// @param builtin the builtin call instruction
-    /// @returns the replacement value
-    core::ir::Value* Atomic(core::ir::CoreBuiltinCall* builtin) {
+    void Atomic(core::ir::CoreBuiltinCall* builtin) {
         auto* result_ty = builtin->Result(0)->Type();
 
         auto* pointer = builtin->Args()[0];
@@ -235,27 +224,29 @@
         auto* memory_semantics = b.Constant(u32(SpvMemorySemanticsMaskNone));
 
         // Helper to build the builtin call with the common operands.
-        auto build = [&](const core::type::Type* type, enum spirv::BuiltinFn builtin_fn) {
-            return b.Call<spirv::ir::BuiltinCall>(type, builtin_fn, pointer, memory,
-                                                  memory_semantics);
+        auto build = [&](enum spirv::BuiltinFn builtin_fn) {
+            return b.CallWithResult<spirv::ir::BuiltinCall>(builtin->DetachResult(), builtin_fn,
+                                                            pointer, memory, memory_semantics);
         };
 
         // Create the replacement call instruction.
         core::ir::Call* call = nullptr;
         switch (builtin->Func()) {
             case core::BuiltinFn::kAtomicAdd:
-                call = build(result_ty, spirv::BuiltinFn::kAtomicIadd);
+                call = build(spirv::BuiltinFn::kAtomicIadd);
                 call->AppendArg(builtin->Args()[1]);
                 break;
             case core::BuiltinFn::kAtomicAnd:
-                call = build(result_ty, spirv::BuiltinFn::kAtomicAnd);
+                call = build(spirv::BuiltinFn::kAtomicAnd);
                 call->AppendArg(builtin->Args()[1]);
                 break;
             case core::BuiltinFn::kAtomicCompareExchangeWeak: {
                 auto* cmp = builtin->Args()[1];
                 auto* value = builtin->Args()[2];
                 auto* int_ty = value->Type();
-                call = build(int_ty, spirv::BuiltinFn::kAtomicCompareExchange);
+                call =
+                    b.Call<spirv::ir::BuiltinCall>(int_ty, spirv::BuiltinFn::kAtomicCompareExchange,
+                                                   pointer, memory, memory_semantics);
                 call->AppendArg(memory_semantics);
                 call->AppendArg(value);
                 call->AppendArg(cmp);
@@ -267,62 +258,60 @@
                 compare->InsertBefore(builtin);
 
                 // Construct the atomicCompareExchange result structure.
-                call = b.Construct(
-                    core::type::CreateAtomicCompareExchangeResult(ty, ir.symbols, int_ty),
-                    Vector{original, compare->Result(0)});
+                call = b.ConstructWithResult(builtin->DetachResult(),
+                                             Vector{original, compare->Result(0)});
                 break;
             }
             case core::BuiltinFn::kAtomicExchange:
-                call = build(result_ty, spirv::BuiltinFn::kAtomicExchange);
+                call = build(spirv::BuiltinFn::kAtomicExchange);
                 call->AppendArg(builtin->Args()[1]);
                 break;
             case core::BuiltinFn::kAtomicLoad:
-                call = build(result_ty, spirv::BuiltinFn::kAtomicLoad);
+                call = build(spirv::BuiltinFn::kAtomicLoad);
                 break;
             case core::BuiltinFn::kAtomicOr:
-                call = build(result_ty, spirv::BuiltinFn::kAtomicOr);
+                call = build(spirv::BuiltinFn::kAtomicOr);
                 call->AppendArg(builtin->Args()[1]);
                 break;
             case core::BuiltinFn::kAtomicMax:
                 if (result_ty->is_signed_integer_scalar()) {
-                    call = build(result_ty, spirv::BuiltinFn::kAtomicSmax);
+                    call = build(spirv::BuiltinFn::kAtomicSmax);
                 } else {
-                    call = build(result_ty, spirv::BuiltinFn::kAtomicUmax);
+                    call = build(spirv::BuiltinFn::kAtomicUmax);
                 }
                 call->AppendArg(builtin->Args()[1]);
                 break;
             case core::BuiltinFn::kAtomicMin:
                 if (result_ty->is_signed_integer_scalar()) {
-                    call = build(result_ty, spirv::BuiltinFn::kAtomicSmin);
+                    call = build(spirv::BuiltinFn::kAtomicSmin);
                 } else {
-                    call = build(result_ty, spirv::BuiltinFn::kAtomicUmin);
+                    call = build(spirv::BuiltinFn::kAtomicUmin);
                 }
                 call->AppendArg(builtin->Args()[1]);
                 break;
             case core::BuiltinFn::kAtomicStore:
-                call = build(result_ty, spirv::BuiltinFn::kAtomicStore);
+                call = build(spirv::BuiltinFn::kAtomicStore);
                 call->AppendArg(builtin->Args()[1]);
                 break;
             case core::BuiltinFn::kAtomicSub:
-                call = build(result_ty, spirv::BuiltinFn::kAtomicIsub);
+                call = build(spirv::BuiltinFn::kAtomicIsub);
                 call->AppendArg(builtin->Args()[1]);
                 break;
             case core::BuiltinFn::kAtomicXor:
-                call = build(result_ty, spirv::BuiltinFn::kAtomicXor);
+                call = build(spirv::BuiltinFn::kAtomicXor);
                 call->AppendArg(builtin->Args()[1]);
                 break;
             default:
-                return nullptr;
+                TINT_UNREACHABLE() << "unhandled atomic builtin";
         }
 
         call->InsertBefore(builtin);
-        return call->Result(0);
+        builtin->Destroy();
     }
 
     /// Handle a `dot()` builtin.
     /// @param builtin the builtin call instruction
-    /// @returns the replacement value
-    core::ir::Value* Dot(core::ir::CoreBuiltinCall* builtin) {
+    void Dot(core::ir::CoreBuiltinCall* builtin) {
         // OpDot only supports floating point operands, so we need to polyfill the integer case.
         // TODO(crbug.com/tint/1267): If SPV_KHR_integer_dot_product is supported, use that instead.
         if (builtin->Result(0)->Type()->is_integer_scalar()) {
@@ -344,38 +333,38 @@
                     }
                 });
             }
-            return sum->Result(0);
+            sum->SetResults(Vector{builtin->DetachResult()});
+            builtin->Destroy();
+            return;
         }
 
         // Replace the builtin call with a call to the spirv.dot intrinsic.
         auto args = Vector<core::ir::Value*, 4>(builtin->Args());
-        auto* call = b.Call<spirv::ir::BuiltinCall>(builtin->Result(0)->Type(),
-                                                    spirv::BuiltinFn::kDot, std::move(args));
+        auto* call = b.CallWithResult<spirv::ir::BuiltinCall>(
+            builtin->DetachResult(), spirv::BuiltinFn::kDot, std::move(args));
         call->InsertBefore(builtin);
-        return call->Result(0);
+        builtin->Destroy();
     }
 
     /// Handle a `dot4{I,U}8Packed()` builtin.
     /// @param builtin the builtin call instruction
-    /// @returns the replacement value
-    core::ir::Value* DotPacked4x8(core::ir::CoreBuiltinCall* builtin) {
+    void DotPacked4x8(core::ir::CoreBuiltinCall* builtin) {
         // Replace the builtin call with a call to the spirv.{s,u}dot intrinsic.
-        auto* type = builtin->Result(0)->Type();
         auto is_signed = builtin->Func() == core::BuiltinFn::kDot4I8Packed;
         auto inst = is_signed ? spirv::BuiltinFn::kSdot : spirv::BuiltinFn::kUdot;
 
         auto args = Vector<core::ir::Value*, 3>(builtin->Args());
         args.Push(Literal(u32(SpvPackedVectorFormatPackedVectorFormat4x8Bit)));
 
-        auto* call = b.Call<spirv::ir::BuiltinCall>(type, inst, std::move(args));
+        auto* call = b.CallWithResult<spirv::ir::BuiltinCall>(builtin->DetachResult(), inst,
+                                                              std::move(args));
         call->InsertBefore(builtin);
-        return call->Result(0);
+        builtin->Destroy();
     }
 
     /// Handle a `select()` builtin.
     /// @param builtin the builtin call instruction
-    /// @returns the replacement value
-    core::ir::Value* Select(core::ir::CoreBuiltinCall* builtin) {
+    void Select(core::ir::CoreBuiltinCall* builtin) {
         // Argument order is different in SPIR-V: (condition, true_operand, false_operand).
         Vector<core::ir::Value*, 4> args = {
             builtin->Args()[2],
@@ -397,10 +386,10 @@
         }
 
         // Replace the builtin call with a call to the spirv.select intrinsic.
-        auto* call = b.Call<spirv::ir::BuiltinCall>(builtin->Result(0)->Type(),
-                                                    spirv::BuiltinFn::kSelect, std::move(args));
+        auto* call = b.CallWithResult<spirv::ir::BuiltinCall>(
+            builtin->DetachResult(), spirv::BuiltinFn::kSelect, std::move(args));
         call->InsertBefore(builtin);
-        return call->Result(0);
+        builtin->Destroy();
     }
 
     /// ImageOperands represents the optional image operands for an image instruction.
@@ -494,8 +483,7 @@
 
     /// Handle a textureSample*() builtin.
     /// @param builtin the builtin call instruction
-    /// @returns the replacement value
-    core::ir::Value* TextureSample(core::ir::CoreBuiltinCall* builtin) {
+    void TextureSample(core::ir::CoreBuiltinCall* builtin) {
         // Helper to get the next argument from the call, or nullptr if there are no more arguments.
         uint32_t arg_idx = 0;
         auto next_arg = [&]() {
@@ -520,7 +508,7 @@
         }
 
         // Determine which SPIR-V function to use and which optional image operands are needed.
-        enum spirv::BuiltinFn function;
+        enum spirv::BuiltinFn function = BuiltinFn::kNone;
         core::ir::Value* depth = nullptr;
         ImageOperands operands;
         switch (builtin->Func()) {
@@ -556,7 +544,7 @@
                 operands.offset = next_arg();
                 break;
             default:
-                return nullptr;
+                TINT_UNREACHABLE() << "unhandled texture sample builtin";
         }
 
         // Start building the argument list for the function.
@@ -575,28 +563,25 @@
         // Call the function.
         // If this is a depth comparison, the result is always f32, otherwise vec4f.
         auto* result_ty = depth ? static_cast<const core::type::Type*>(ty.f32()) : ty.vec4<f32>();
-        auto* texture_call =
+        core::ir::Instruction* result =
             b.Call<spirv::ir::BuiltinCall>(result_ty, function, std::move(function_args));
-        texture_call->InsertBefore(builtin);
-
-        auto* result = texture_call->Result(0);
+        result->InsertBefore(builtin);
 
         // If this is not a depth comparison but we are sampling a depth texture, extract the first
         // component to get the scalar f32 that SPIR-V expects.
         if (!depth &&
             texture_ty->IsAnyOf<core::type::DepthTexture, core::type::DepthMultisampledTexture>()) {
-            auto* extract = b.Access(ty.f32(), result, 0_u);
-            extract->InsertBefore(builtin);
-            result = extract->Result(0);
+            result = b.Access(ty.f32(), result, 0_u);
+            result->InsertBefore(builtin);
         }
 
-        return result;
+        result->SetResults(Vector{builtin->DetachResult()});
+        builtin->Destroy();
     }
 
     /// Handle a textureGather*() builtin.
     /// @param builtin the builtin call instruction
-    /// @returns the replacement value
-    core::ir::Value* TextureGather(core::ir::CoreBuiltinCall* builtin) {
+    void TextureGather(core::ir::CoreBuiltinCall* builtin) {
         // Helper to get the next argument from the call, or nullptr if there are no more arguments.
         uint32_t arg_idx = 0;
         auto next_arg = [&]() {
@@ -628,7 +613,7 @@
         }
 
         // Determine which SPIR-V function to use and which optional image operands are needed.
-        enum spirv::BuiltinFn function;
+        enum spirv::BuiltinFn function = BuiltinFn::kNone;
         core::ir::Value* depth = nullptr;
         ImageOperands operands;
         switch (builtin->Func()) {
@@ -642,7 +627,7 @@
                 operands.offset = next_arg();
                 break;
             default:
-                return nullptr;
+                TINT_UNIMPLEMENTED() << "unhandled texture gather builtin";
         }
 
         // Start building the argument list for the function.
@@ -661,17 +646,15 @@
         AppendImageOperands(operands, function_args, builtin, /* requires_float_lod */ true);
 
         // Call the function.
-        auto* result_ty = builtin->Result(0)->Type();
-        auto* texture_call =
-            b.Call<spirv::ir::BuiltinCall>(result_ty, function, std::move(function_args));
+        auto* texture_call = b.CallWithResult<spirv::ir::BuiltinCall>(
+            builtin->DetachResult(), function, std::move(function_args));
         texture_call->InsertBefore(builtin);
-        return texture_call->Result(0);
+        builtin->Destroy();
     }
 
     /// Handle a textureLoad() builtin.
     /// @param builtin the builtin call instruction
-    /// @returns the replacement value
-    core::ir::Value* TextureLoad(core::ir::CoreBuiltinCall* builtin) {
+    void TextureLoad(core::ir::CoreBuiltinCall* builtin) {
         // Helper to get the next argument from the call, or nullptr if there are no more arguments.
         uint32_t arg_idx = 0;
         auto next_arg = [&]() {
@@ -713,25 +696,23 @@
         }
         auto kind = texture_ty->Is<core::type::StorageTexture>() ? spirv::BuiltinFn::kImageRead
                                                                  : spirv::BuiltinFn::kImageFetch;
-        auto* texture_call =
+        core::ir::Instruction* result =
             b.Call<spirv::ir::BuiltinCall>(result_ty, kind, std::move(builtin_args));
-        texture_call->InsertBefore(builtin);
-        auto* result = texture_call->Result(0);
+        result->InsertBefore(builtin);
 
         // If we are expecting a scalar result, extract the first component.
         if (expects_scalar_result) {
-            auto* extract = b.Access(ty.f32(), result, 0_u);
-            extract->InsertBefore(builtin);
-            result = extract->Result(0);
+            result = b.Access(ty.f32(), result, 0_u);
+            result->InsertBefore(builtin);
         }
 
-        return result;
+        result->SetResults(Vector{builtin->DetachResult()});
+        builtin->Destroy();
     }
 
     /// Handle a textureStore() builtin.
     /// @param builtin the builtin call instruction
-    /// @returns the replacement value
-    core::ir::Value* TextureStore(core::ir::CoreBuiltinCall* builtin) {
+    void TextureStore(core::ir::CoreBuiltinCall* builtin) {
         // Helper to get the next argument from the call, or nullptr if there are no more arguments.
         uint32_t arg_idx = 0;
         auto next_arg = [&]() {
@@ -764,13 +745,12 @@
         auto* texture_call = b.Call<spirv::ir::BuiltinCall>(
             ty.void_(), spirv::BuiltinFn::kImageWrite, std::move(function_args));
         texture_call->InsertBefore(builtin);
-        return texture_call->Result(0);
+        builtin->Destroy();
     }
 
     /// Handle a textureDimensions() builtin.
     /// @param builtin the builtin call instruction
-    /// @returns the replacement value
-    core::ir::Value* TextureDimensions(core::ir::CoreBuiltinCall* builtin) {
+    void TextureDimensions(core::ir::CoreBuiltinCall* builtin) {
         // Helper to get the next argument from the call, or nullptr if there are no more arguments.
         uint32_t arg_idx = 0;
         auto next_arg = [&]() {
@@ -807,26 +787,23 @@
         }
 
         // Call the function.
-        auto* texture_call =
+        core::ir::Instruction* result =
             b.Call<spirv::ir::BuiltinCall>(result_ty, function, std::move(function_args));
-        texture_call->InsertBefore(builtin);
-
-        auto* result = texture_call->Result(0);
+        result->InsertBefore(builtin);
 
         // Swizzle the first two components from the result for arrayed textures.
         if (core::type::IsTextureArray(texture_ty->dim())) {
-            auto* swizzle = b.Swizzle(builtin->Result(0)->Type(), result, {0, 1});
-            swizzle->InsertBefore(builtin);
-            result = swizzle->Result(0);
+            result = b.Swizzle(builtin->Result(0)->Type(), result, {0, 1});
+            result->InsertBefore(builtin);
         }
 
-        return result;
+        result->SetResults(Vector{builtin->DetachResult()});
+        builtin->Destroy();
     }
 
     /// Handle a textureNumLayers() builtin.
     /// @param builtin the builtin call instruction
-    /// @returns the replacement value
-    core::ir::Value* TextureNumLayers(core::ir::CoreBuiltinCall* builtin) {
+    void TextureNumLayers(core::ir::CoreBuiltinCall* builtin) {
         auto* texture = builtin->Args()[0];
         auto* texture_ty = texture->Type()->As<core::type::Texture>();
 
@@ -850,16 +827,15 @@
         texture_call->InsertBefore(builtin);
 
         // Extract the third component to get the number of array layers.
-        auto* extract = b.Access(ty.u32(), texture_call->Result(0), 2_u);
+        auto* extract = b.AccessWithResult(builtin->DetachResult(), texture_call->Result(0), 2_u);
         extract->InsertBefore(builtin);
-        return extract->Result(0);
+        builtin->Destroy();
     }
 
     /// Scalarize the vector form of a `quantizeToF16()` builtin.
     /// See crbug.com/tint/1741.
     /// @param builtin the builtin call instruction
-    /// @returns the replacement value
-    core::ir::Value* QuantizeToF16Vec(core::ir::CoreBuiltinCall* builtin) {
+    void QuantizeToF16Vec(core::ir::CoreBuiltinCall* builtin) {
         auto* arg = builtin->Args()[0];
         auto* vec = arg->Type()->As<core::type::Vector>();
         TINT_ASSERT(vec);
@@ -873,9 +849,9 @@
             el->InsertBefore(builtin);
             scalar_call->InsertBefore(builtin);
         }
-        auto* construct = b.Construct(vec, std::move(args));
+        auto* construct = b.ConstructWithResult(builtin->DetachResult(), std::move(args));
         construct->InsertBefore(builtin);
-        return construct->Result(0);
+        builtin->Destroy();
     }
 };
 
diff --git a/src/tint/lang/spirv/writer/raise/expand_implicit_splats.cc b/src/tint/lang/spirv/writer/raise/expand_implicit_splats.cc
index 3b650a7..c5359f2 100644
--- a/src/tint/lang/spirv/writer/raise/expand_implicit_splats.cc
+++ b/src/tint/lang/spirv/writer/raise/expand_implicit_splats.cc
@@ -107,8 +107,8 @@
         auto* result_ty = binary->Result(0)->Type();
         if (result_ty->is_float_vector() && binary->Op() == core::BinaryOp::kMultiply) {
             // Use OpVectorTimesScalar for floating point multiply.
-            auto* vts =
-                b.Call<spirv::ir::BuiltinCall>(result_ty, spirv::BuiltinFn::kVectorTimesScalar);
+            auto* vts = b.CallWithResult<spirv::ir::BuiltinCall>(
+                binary->DetachResult(), spirv::BuiltinFn::kVectorTimesScalar);
             if (binary->LHS()->Type()->Is<core::type::Scalar>()) {
                 vts->AppendArg(binary->RHS());
                 vts->AppendArg(binary->LHS());
@@ -119,7 +119,6 @@
             if (auto name = ir.NameOf(binary)) {
                 ir.SetName(vts->Result(0), name);
             }
-            binary->Result(0)->ReplaceAllUsesWith(vts->Result(0));
             binary->ReplaceWith(vts);
             binary->Destroy();
         } else {
diff --git a/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.cc b/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.cc
index 487bac0..3091c09 100644
--- a/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.cc
+++ b/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.cc
@@ -77,65 +77,63 @@
         auto* rhs_ty = rhs->Type();
         auto* ty = binary->Result(0)->Type();
 
-        // Helper to replace the instruction with a new one.
-        auto replace = [&](core::ir::Instruction* inst) {
-            if (auto name = ir.NameOf(binary)) {
-                ir.SetName(inst->Result(0), name);
-            }
-            binary->Result(0)->ReplaceAllUsesWith(inst->Result(0));
-            binary->ReplaceWith(inst);
-            binary->Destroy();
-        };
-
-        // Helper to replace the instruction with a column-wise operation.
-        auto column_wise = [&](auto op) {
-            auto* mat = ty->As<core::type::Matrix>();
-            Vector<core::ir::Value*, 4> args;
-            for (uint32_t col = 0; col < mat->columns(); col++) {
-                b.InsertBefore(binary, [&] {
+        b.InsertBefore(binary, [&] {
+            // Helper to replace the instruction with a column-wise operation.
+            auto column_wise = [&](auto op) {
+                auto* mat = ty->As<core::type::Matrix>();
+                Vector<core::ir::Value*, 4> args;
+                for (uint32_t col = 0; col < mat->columns(); col++) {
                     auto* lhs_col = b.Access(mat->ColumnType(), lhs, u32(col));
                     auto* rhs_col = b.Access(mat->ColumnType(), rhs, u32(col));
                     auto* add = b.Binary(op, mat->ColumnType(), lhs_col, rhs_col);
                     args.Push(add->Result(0));
-                });
-            }
-            replace(b.Construct(ty, std::move(args)));
-        };
-
-        switch (binary->Op()) {
-            case core::BinaryOp::kAdd:
-                column_wise(core::BinaryOp::kAdd);
-                break;
-            case core::BinaryOp::kSubtract:
-                column_wise(core::BinaryOp::kSubtract);
-                break;
-            case core::BinaryOp::kMultiply:
-                // Select the SPIR-V intrinsic that corresponds to the operation being performed.
-                if (lhs_ty->Is<core::type::Matrix>()) {
-                    if (rhs_ty->Is<core::type::Scalar>()) {
-                        replace(b.Call<spirv::ir::BuiltinCall>(
-                            ty, spirv::BuiltinFn::kMatrixTimesScalar, lhs, rhs));
-                    } else if (rhs_ty->Is<core::type::Vector>()) {
-                        replace(b.Call<spirv::ir::BuiltinCall>(
-                            ty, spirv::BuiltinFn::kMatrixTimesVector, lhs, rhs));
-                    } else if (rhs_ty->Is<core::type::Matrix>()) {
-                        replace(b.Call<spirv::ir::BuiltinCall>(
-                            ty, spirv::BuiltinFn::kMatrixTimesMatrix, lhs, rhs));
-                    }
-                } else {
-                    if (lhs_ty->Is<core::type::Scalar>()) {
-                        replace(b.Call<spirv::ir::BuiltinCall>(
-                            ty, spirv::BuiltinFn::kMatrixTimesScalar, rhs, lhs));
-                    } else if (lhs_ty->Is<core::type::Vector>()) {
-                        replace(b.Call<spirv::ir::BuiltinCall>(
-                            ty, spirv::BuiltinFn::kVectorTimesMatrix, lhs, rhs));
-                    }
                 }
-                break;
+                b.ConstructWithResult(binary->DetachResult(), std::move(args));
+            };
 
-            default:
-                TINT_UNREACHABLE() << "unhandled matrix arithmetic instruction";
-        }
+            switch (binary->Op()) {
+                case core::BinaryOp::kAdd:
+                    column_wise(core::BinaryOp::kAdd);
+                    break;
+                case core::BinaryOp::kSubtract:
+                    column_wise(core::BinaryOp::kSubtract);
+                    break;
+                case core::BinaryOp::kMultiply:
+                    // Select the SPIR-V intrinsic that corresponds to the operation being
+                    // performed.
+                    if (lhs_ty->Is<core::type::Matrix>()) {
+                        if (rhs_ty->Is<core::type::Scalar>()) {
+                            b.CallWithResult<spirv::ir::BuiltinCall>(
+                                binary->DetachResult(), spirv::BuiltinFn::kMatrixTimesScalar, lhs,
+                                rhs);
+                        } else if (rhs_ty->Is<core::type::Vector>()) {
+                            b.CallWithResult<spirv::ir::BuiltinCall>(
+                                binary->DetachResult(), spirv::BuiltinFn::kMatrixTimesVector, lhs,
+                                rhs);
+                        } else if (rhs_ty->Is<core::type::Matrix>()) {
+                            b.CallWithResult<spirv::ir::BuiltinCall>(
+                                binary->DetachResult(), spirv::BuiltinFn::kMatrixTimesMatrix, lhs,
+                                rhs);
+                        }
+                    } else {
+                        if (lhs_ty->Is<core::type::Scalar>()) {
+                            b.CallWithResult<spirv::ir::BuiltinCall>(
+                                binary->DetachResult(), spirv::BuiltinFn::kMatrixTimesScalar, rhs,
+                                lhs);
+                        } else if (lhs_ty->Is<core::type::Vector>()) {
+                            b.CallWithResult<spirv::ir::BuiltinCall>(
+                                binary->DetachResult(), spirv::BuiltinFn::kVectorTimesMatrix, lhs,
+                                rhs);
+                        }
+                    }
+                    break;
+
+                default:
+                    TINT_UNREACHABLE() << "unhandled matrix arithmetic instruction";
+            }
+        });
+
+        binary->Destroy();
     }
 
     /// Replace a matrix convert instruction.
@@ -145,23 +143,19 @@
         auto* in_mat = arg->Type()->As<core::type::Matrix>();
         auto* out_mat = convert->Result(0)->Type()->As<core::type::Matrix>();
 
-        // Extract and convert each column separately.
-        Vector<core::ir::Value*, 4> args;
-        for (uint32_t c = 0; c < out_mat->columns(); c++) {
-            b.InsertBefore(convert, [&] {
+        b.InsertBefore(convert, [&] {
+            // Extract and convert each column separately.
+            Vector<core::ir::Value*, 4> args;
+            for (uint32_t c = 0; c < out_mat->columns(); c++) {
                 auto* col = b.Access(in_mat->ColumnType(), arg, u32(c));
                 auto* new_col = b.Convert(out_mat->ColumnType(), col);
                 args.Push(new_col->Result(0));
-            });
-        }
+            }
 
-        // Reconstruct the result matrix from the converted columns.
-        auto* construct = b.Construct(out_mat, std::move(args));
-        if (auto name = ir.NameOf(convert)) {
-            ir.SetName(construct->Result(0), name);
-        }
-        convert->Result(0)->ReplaceAllUsesWith(construct->Result(0));
-        convert->ReplaceWith(construct);
+            // Reconstruct the result matrix from the converted columns.
+            b.ConstructWithResult(convert->DetachResult(), std::move(args));
+        });
+
         convert->Destroy();
     }
 };
diff --git a/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc b/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
index 409a52e..49655d5 100644
--- a/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
+++ b/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
@@ -237,13 +237,11 @@
 
             core::ir::Instruction* load = nullptr;
             if (to_replace.vector_access_type) {
-                load = b.LoadVectorElement(new_access->Result(0), vector_index);
+                load = b.LoadVectorElementWithResult(access->DetachResult(), new_access->Result(0),
+                                                     vector_index);
             } else {
-                load = b.Load(new_access);
+                load = b.LoadWithResult(access->DetachResult(), new_access);
             }
-
-            // Replace all uses of the old access instruction with the loaded result.
-            access->Result(0)->ReplaceAllUsesWith(load->Result(0));
             access->ReplaceWith(load);
             access->Destroy();
         }
diff --git a/src/tint/lang/wgsl/reader/lower/lower.cc b/src/tint/lang/wgsl/reader/lower/lower.cc
index 0752652..33f1999 100644
--- a/src/tint/lang/wgsl/reader/lower/lower.cc
+++ b/src/tint/lang/wgsl/reader/lower/lower.cc
@@ -198,19 +198,16 @@
                     //    call workgroupBarrier
                     b.InsertBefore(call, [&] {
                         b.Call(ty.void_(), core::BuiltinFn::kWorkgroupBarrier);
-                        auto* load = b.Load(call->Args()[0]);
-                        call->Result(0)->ReplaceAllUsesWith(load->Result(0));
+                        b.LoadWithResult(call->DetachResult(), call->Args()[0]);
                         b.Call(ty.void_(), core::BuiltinFn::kWorkgroupBarrier);
                     });
                     break;
                 }
                 default: {
                     Vector<core::ir::Value*, 8> args(call->Args());
-                    auto* replacement =
-                        mod.allocators.instructions.Create<core::ir::CoreBuiltinCall>(
-                            call->Result(0), Convert(call->Func()), std::move(args));
+                    auto* replacement = b.CallWithResult(call->DetachResult(),
+                                                         Convert(call->Func()), std::move(args));
                     call->ReplaceWith(replacement);
-                    call->ClearResults();
                     break;
                 }
             }
diff --git a/src/tint/lang/wgsl/writer/raise/raise.cc b/src/tint/lang/wgsl/writer/raise/raise.cc
index c576ac4..d06cb72 100644
--- a/src/tint/lang/wgsl/writer/raise/raise.cc
+++ b/src/tint/lang/wgsl/writer/raise/raise.cc
@@ -30,6 +30,7 @@
 #include <utility>
 
 #include "src/tint/lang/core/builtin_fn.h"
+#include "src/tint/lang/core/ir/builder.h"
 #include "src/tint/lang/core/ir/core_builtin_call.h"
 #include "src/tint/lang/core/ir/load.h"
 #include "src/tint/lang/core/type/pointer.h"
@@ -175,16 +176,15 @@
     TINT_ICE() << "unhandled builtin function: " << fn;
 }
 
-void ReplaceBuiltinFnCall(core::ir::Module& mod, core::ir::CoreBuiltinCall* call) {
+void ReplaceBuiltinFnCall(core::ir::Builder& b, core::ir::CoreBuiltinCall* call) {
     Vector<core::ir::Value*, 8> args(call->Args());
-    auto* replacement = mod.allocators.instructions.Create<wgsl::ir::BuiltinCall>(
-        call->Result(0), Convert(call->Func()), std::move(args));
+    auto* replacement = b.CallWithResult<wgsl::ir::BuiltinCall>(
+        call->DetachResult(), Convert(call->Func()), std::move(args));
     call->ReplaceWith(replacement);
-    call->ClearResults();
     call->Destroy();
 }
 
-void ReplaceWorkgroupBarrier(core::ir::Module& mod, core::ir::CoreBuiltinCall* call) {
+void ReplaceWorkgroupBarrier(core::ir::Builder& b, core::ir::CoreBuiltinCall* call) {
     // Pattern match:
     //    call workgroupBarrier
     //    %value = load &ptr
@@ -196,14 +196,14 @@
     if (!load || load->From()->Type()->As<core::type::Pointer>()->AddressSpace() !=
                      core::AddressSpace::kWorkgroup) {
         // No match
-        ReplaceBuiltinFnCall(mod, call);
+        ReplaceBuiltinFnCall(b, call);
         return;
     }
 
     auto* post_load = As<core::ir::CoreBuiltinCall>(load->next.Get());
     if (!post_load || post_load->Func() != core::BuiltinFn::kWorkgroupBarrier) {
         // No match
-        ReplaceBuiltinFnCall(mod, call);
+        ReplaceBuiltinFnCall(b, call);
         return;
     }
 
@@ -212,24 +212,24 @@
     call->Destroy();
 
     // Replace load with workgroupUniformLoad
-    auto* replacement = mod.allocators.instructions.Create<wgsl::ir::BuiltinCall>(
-        load->Result(0), wgsl::BuiltinFn::kWorkgroupUniformLoad, Vector{load->From()});
+    auto* replacement = b.CallWithResult<wgsl::ir::BuiltinCall>(
+        load->DetachResult(), wgsl::BuiltinFn::kWorkgroupUniformLoad, Vector{load->From()});
     load->ReplaceWith(replacement);
-    load->ClearResults();
     load->Destroy();
 }
 
 }  // namespace
 
 Result<SuccessType> Raise(core::ir::Module& mod) {
+    core::ir::Builder b{mod};
     for (auto* inst : mod.Instructions()) {
         if (auto* call = inst->As<core::ir::CoreBuiltinCall>()) {
             switch (call->Func()) {
                 case core::BuiltinFn::kWorkgroupBarrier:
-                    ReplaceWorkgroupBarrier(mod, call);
+                    ReplaceWorkgroupBarrier(b, call);
                     break;
                 default:
-                    ReplaceBuiltinFnCall(mod, call);
+                    ReplaceBuiltinFnCall(b, call);
                     break;
             }
         }