[ir] Add single entry point transform

This will not be useful for production flows until pipeline-override
support is added, but can still be used by the GLSL fuzzer.

Fixed: 374972031
Change-Id: If16f60ac7763e79a8abc6e81357df1db7c7b2197
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/211796
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Commit-Queue: James Price <jrprice@google.com>
diff --git a/src/tint/lang/core/ir/referenced_module_vars.h b/src/tint/lang/core/ir/referenced_module_vars.h
index 47204ea..bdf1532 100644
--- a/src/tint/lang/core/ir/referenced_module_vars.h
+++ b/src/tint/lang/core/ir/referenced_module_vars.h
@@ -30,6 +30,7 @@
 
 #include <functional>
 
+#include "src/tint/lang/core/ir/control_instruction.h"
 #include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/user_call.h"
 #include "src/tint/lang/core/ir/var.h"
diff --git a/src/tint/lang/core/ir/transform/BUILD.bazel b/src/tint/lang/core/ir/transform/BUILD.bazel
index d303af5..e82b9c7 100644
--- a/src/tint/lang/core/ir/transform/BUILD.bazel
+++ b/src/tint/lang/core/ir/transform/BUILD.bazel
@@ -58,6 +58,7 @@
     "rename_conflicts.cc",
     "robustness.cc",
     "shader_io.cc",
+    "single_entry_point.cc",
     "std140.cc",
     "value_to_let.cc",
     "vectorize_scalar_matrix_constructors.cc",
@@ -83,6 +84,7 @@
     "rename_conflicts.h",
     "robustness.h",
     "shader_io.h",
+    "single_entry_point.h",
     "std140.h",
     "value_to_let.h",
     "vectorize_scalar_matrix_constructors.h",
@@ -137,6 +139,7 @@
     "remove_terminator_args_test.cc",
     "rename_conflicts_test.cc",
     "robustness_test.cc",
+    "single_entry_point_test.cc",
     "std140_test.cc",
     "value_to_let_test.cc",
     "vectorize_scalar_matrix_constructors_test.cc",
diff --git a/src/tint/lang/core/ir/transform/BUILD.cmake b/src/tint/lang/core/ir/transform/BUILD.cmake
index 3d32225..5496c4b 100644
--- a/src/tint/lang/core/ir/transform/BUILD.cmake
+++ b/src/tint/lang/core/ir/transform/BUILD.cmake
@@ -77,6 +77,8 @@
   lang/core/ir/transform/robustness.h
   lang/core/ir/transform/shader_io.cc
   lang/core/ir/transform/shader_io.h
+  lang/core/ir/transform/single_entry_point.cc
+  lang/core/ir/transform/single_entry_point.h
   lang/core/ir/transform/std140.cc
   lang/core/ir/transform/std140.h
   lang/core/ir/transform/value_to_let.cc
@@ -138,6 +140,7 @@
   lang/core/ir/transform/remove_terminator_args_test.cc
   lang/core/ir/transform/rename_conflicts_test.cc
   lang/core/ir/transform/robustness_test.cc
+  lang/core/ir/transform/single_entry_point_test.cc
   lang/core/ir/transform/std140_test.cc
   lang/core/ir/transform/value_to_let_test.cc
   lang/core/ir/transform/vectorize_scalar_matrix_constructors_test.cc
diff --git a/src/tint/lang/core/ir/transform/BUILD.gn b/src/tint/lang/core/ir/transform/BUILD.gn
index 900e4da..d3cbca3 100644
--- a/src/tint/lang/core/ir/transform/BUILD.gn
+++ b/src/tint/lang/core/ir/transform/BUILD.gn
@@ -83,6 +83,8 @@
     "robustness.h",
     "shader_io.cc",
     "shader_io.h",
+    "single_entry_point.cc",
+    "single_entry_point.h",
     "std140.cc",
     "std140.h",
     "value_to_let.cc",
@@ -138,6 +140,7 @@
       "remove_terminator_args_test.cc",
       "rename_conflicts_test.cc",
       "robustness_test.cc",
+      "single_entry_point_test.cc",
       "std140_test.cc",
       "value_to_let_test.cc",
       "vectorize_scalar_matrix_constructors_test.cc",
diff --git a/src/tint/lang/core/ir/transform/single_entry_point.cc b/src/tint/lang/core/ir/transform/single_entry_point.cc
new file mode 100644
index 0000000..2282813
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/single_entry_point.cc
@@ -0,0 +1,102 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/transform/single_entry_point.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/module.h"
+#include "src/tint/lang/core/ir/referenced_functions.h"
+#include "src/tint/lang/core/ir/referenced_module_vars.h"
+#include "src/tint/lang/core/ir/validator.h"
+
+namespace tint::core::ir::transform {
+
+namespace {
+
+Result<SuccessType> Run(ir::Module& ir, std::string_view entry_point_name) {
+    // Find the entry point.
+    ir::Function* entry_point = nullptr;
+    for (auto& func : ir.functions) {
+        if (func->Stage() == Function::PipelineStage::kUndefined) {
+            continue;
+        }
+        if (ir.NameOf(func).NameView() == entry_point_name) {
+            if (entry_point) {
+                TINT_ICE() << "multiple entry points named '" << entry_point_name << "' were found";
+            }
+            entry_point = func;
+        }
+    }
+    if (!entry_point) {
+        TINT_ICE() << "entry point '" << entry_point_name << "' not found";
+    }
+
+    // Remove unused functions.
+    ReferencedFunctions<Module> referenced_function_cache(ir);
+    auto& referenced_functions = referenced_function_cache.TransitiveReferences(entry_point);
+    for (uint32_t i = 0; i < ir.functions.Length();) {
+        auto func = ir.functions[i];
+        if (func == entry_point || referenced_functions.Contains(func)) {
+            i++;
+            continue;
+        }
+
+        func->Destroy();
+        ir.functions.Erase(i);
+    }
+
+    // Remove unused module-scope variables.
+    ReferencedModuleVars<Module> referenced_var_cache(ir);
+    auto& referenced_vars = referenced_var_cache.TransitiveReferences(entry_point);
+    for (auto* decl : *ir.root_block) {
+        // Assume that we only have var instructions for now, until override support is added.
+        auto* var = decl->As<Var>();
+        TINT_ASSERT(var);
+        if (!referenced_vars.Contains(var)) {
+            // There shouldn't be any remaining references to the variable.
+            // This will not always be the case once we have override support.
+            TINT_ASSERT(var->Result(0)->NumUsages() == 0);
+            var->Destroy();
+        }
+    }
+
+    return Success;
+}
+
+}  // namespace
+
+Result<SuccessType> SingleEntryPoint(Module& ir, std::string_view entry_point_name) {
+    auto result = ValidateAndDumpIfNeeded(ir, "SingleEntryPoint transform");
+    if (result != Success) {
+        return result.Failure();
+    }
+
+    return Run(ir, entry_point_name);
+}
+
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/single_entry_point.h b/src/tint/lang/core/ir/transform/single_entry_point.h
new file mode 100644
index 0000000..2ce152b
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/single_entry_point.h
@@ -0,0 +1,49 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_CORE_IR_TRANSFORM_SINGLE_ENTRY_POINT_H_
+#define SRC_TINT_LANG_CORE_IR_TRANSFORM_SINGLE_ENTRY_POINT_H_
+
+#include "src/tint/utils/result/result.h"
+
+// Forward declarations.
+namespace tint::core::ir {
+class Module;
+}
+
+namespace tint::core::ir::transform {
+
+/// Strip a module down to a single entry point, removing any unused functions and module-scope
+/// declarations.
+/// @param module the module to transform
+/// @param entry_point_name the entry point name
+/// @returns success or failure
+Result<SuccessType> SingleEntryPoint(Module& module, std::string_view entry_point_name);
+
+}  // namespace tint::core::ir::transform
+
+#endif  // SRC_TINT_LANG_CORE_IR_TRANSFORM_SINGLE_ENTRY_POINT_H_
diff --git a/src/tint/lang/core/ir/transform/single_entry_point_test.cc b/src/tint/lang/core/ir/transform/single_entry_point_test.cc
new file mode 100644
index 0000000..d969f0a
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/single_entry_point_test.cc
@@ -0,0 +1,473 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/transform/single_entry_point.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+
+using namespace tint::core::fluent_types;  // NOLINT
+
+namespace tint::core::ir::transform {
+namespace {
+
+class IR_SingleEntryPointTest : public TransformTest {
+  protected:
+    /// @returns a new entry point called @p name that references @p refs
+    Function* EntryPoint(const char* name, std::initializer_list<Value*> refs = {}) {
+        auto* func = Func(name, std::move(refs));
+        func->SetStage(Function::PipelineStage::kFragment);
+        return func;
+    }
+
+    /// @returns a new function called @p name that references @p refs
+    Function* Func(const char* name, std::initializer_list<Value*> refs = {}) {
+        auto* func = b.Function(name, ty.void_());
+        b.Append(func->Block(), [&] {
+            for (auto* ref : refs) {
+                if (auto* f = ref->As<Function>()) {
+                    b.Call(f);
+                } else {
+                    b.Let(ref->Type())->SetValue(ref);
+                }
+            }
+            b.Return(func);
+        });
+        return func;
+    }
+
+    /// @returns a new module-scope variable called @p name
+    InstructionResult* Var(const char* name) {
+        auto* var = b.Var<private_, i32>(name);
+        mod.root_block->Append(var);
+        return var->Result(0);
+    }
+};
+using IR_SingleEntryPointDeathTest = IR_SingleEntryPointTest;
+
+TEST_F(IR_SingleEntryPointTest, EntryPointNotFound) {
+    EntryPoint("main");
+
+    auto* src = R"(
+%main = @fragment func():void {
+  $B1: {
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    EXPECT_DEATH_IF_SUPPORTED({ Run(SingleEntryPoint, "foo"); }, "internal compiler error");
+}
+
+TEST_F(IR_SingleEntryPointTest, MultipleEntryPointsMatch) {
+    EntryPoint("main");
+    EntryPoint("main");
+
+    auto* src = R"(
+%main = @fragment func():void {
+  $B1: {
+    ret
+  }
+}
+%main_1 = @fragment func():void {  # %main_1: 'main'
+  $B2: {
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    EXPECT_DEATH_IF_SUPPORTED({ Run(SingleEntryPoint, "main"); }, "internal compiler error");
+}
+
+TEST_F(IR_SingleEntryPointTest, NoChangesNeeded) {
+    EntryPoint("main");
+
+    auto* src = R"(
+%main = @fragment func():void {
+  $B1: {
+    ret
+  }
+}
+)";
+
+    auto* expect = src;
+
+    EXPECT_EQ(src, str());
+
+    Run(SingleEntryPoint, "main");
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_SingleEntryPointTest, TwoEntryPoints) {
+    EntryPoint("foo");
+    EntryPoint("bar");
+
+    auto* src = R"(
+%foo = @fragment func():void {
+  $B1: {
+    ret
+  }
+}
+%bar = @fragment func():void {
+  $B2: {
+    ret
+  }
+}
+)";
+
+    auto* expect = R"(
+%foo = @fragment func():void {
+  $B1: {
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    Run(SingleEntryPoint, "foo");
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_SingleEntryPointTest, DirectFunctionCalls) {
+    auto* f1 = Func("f1");
+    auto* f2 = Func("f2");
+    auto* f3 = Func("f3");
+
+    EntryPoint("foo", {f1, f2});
+    EntryPoint("bar", {f3});
+
+    auto* src = R"(
+%f1 = func():void {
+  $B1: {
+    ret
+  }
+}
+%f2 = func():void {
+  $B2: {
+    ret
+  }
+}
+%f3 = func():void {
+  $B3: {
+    ret
+  }
+}
+%foo = @fragment func():void {
+  $B4: {
+    %5:void = call %f1
+    %6:void = call %f2
+    ret
+  }
+}
+%bar = @fragment func():void {
+  $B5: {
+    %8:void = call %f3
+    ret
+  }
+}
+)";
+
+    auto* expect = R"(
+%f1 = func():void {
+  $B1: {
+    ret
+  }
+}
+%f2 = func():void {
+  $B2: {
+    ret
+  }
+}
+%foo = @fragment func():void {
+  $B3: {
+    %4:void = call %f1
+    %5:void = call %f2
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    Run(SingleEntryPoint, "foo");
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_SingleEntryPointTest, DirectVariables) {
+    auto* v1 = Var("v1");
+    auto* v2 = Var("v2");
+    auto* v3 = Var("v3");
+
+    EntryPoint("foo", {v1, v2});
+    EntryPoint("bar", {v3});
+
+    auto* src = R"(
+$B1: {  # root
+  %v1:ptr<private, i32, read_write> = var
+  %v2:ptr<private, i32, read_write> = var
+  %v3:ptr<private, i32, read_write> = var
+}
+
+%foo = @fragment func():void {
+  $B2: {
+    %5:ptr<private, i32, read_write> = let %v1
+    %6:ptr<private, i32, read_write> = let %v2
+    ret
+  }
+}
+%bar = @fragment func():void {
+  $B3: {
+    %8:ptr<private, i32, read_write> = let %v3
+    ret
+  }
+}
+)";
+
+    auto* expect = R"(
+$B1: {  # root
+  %v1:ptr<private, i32, read_write> = var
+  %v2:ptr<private, i32, read_write> = var
+}
+
+%foo = @fragment func():void {
+  $B2: {
+    %4:ptr<private, i32, read_write> = let %v1
+    %5:ptr<private, i32, read_write> = let %v2
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    Run(SingleEntryPoint, "foo");
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_SingleEntryPointTest, TransitiveReferences) {
+    Var("unused_var");
+    Func("unused_func");
+
+    auto* v1 = Var("v1");
+    auto* v2 = Var("v2");
+    auto* v3 = Var("v3");
+
+    auto* f1 = Func("f1", {v2, v3});
+    auto* f2 = Func("f2", {f1});
+    auto* f3 = Func("f3", {v1, f2});
+
+    EntryPoint("foo", {f3});
+
+    auto* src = R"(
+$B1: {  # root
+  %unused_var:ptr<private, i32, read_write> = var
+  %v1:ptr<private, i32, read_write> = var
+  %v2:ptr<private, i32, read_write> = var
+  %v3:ptr<private, i32, read_write> = var
+}
+
+%unused_func = func():void {
+  $B2: {
+    ret
+  }
+}
+%f1 = func():void {
+  $B3: {
+    %7:ptr<private, i32, read_write> = let %v2
+    %8:ptr<private, i32, read_write> = let %v3
+    ret
+  }
+}
+%f2 = func():void {
+  $B4: {
+    %10:void = call %f1
+    ret
+  }
+}
+%f3 = func():void {
+  $B5: {
+    %12:ptr<private, i32, read_write> = let %v1
+    %13:void = call %f2
+    ret
+  }
+}
+%foo = @fragment func():void {
+  $B6: {
+    %15:void = call %f3
+    ret
+  }
+}
+)";
+
+    auto* expect = R"(
+$B1: {  # root
+  %v1:ptr<private, i32, read_write> = var
+  %v2:ptr<private, i32, read_write> = var
+  %v3:ptr<private, i32, read_write> = var
+}
+
+%f1 = func():void {
+  $B2: {
+    %5:ptr<private, i32, read_write> = let %v2
+    %6:ptr<private, i32, read_write> = let %v3
+    ret
+  }
+}
+%f2 = func():void {
+  $B3: {
+    %8:void = call %f1
+    ret
+  }
+}
+%f3 = func():void {
+  $B4: {
+    %10:ptr<private, i32, read_write> = let %v1
+    %11:void = call %f2
+    ret
+  }
+}
+%foo = @fragment func():void {
+  $B5: {
+    %13:void = call %f3
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    Run(SingleEntryPoint, "foo");
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_SingleEntryPointTest, RemoveMultipleFunctions) {
+    auto* f1 = Func("f1");
+    auto* f2 = Func("f2");
+    auto* f3 = Func("f3");
+    auto* f4 = Func("f4");
+    auto* f5 = Func("f5");
+    auto* f6 = Func("f6");
+    auto* f7 = Func("f7");
+
+    EntryPoint("foo", {f1, f5});
+    EntryPoint("bar", {f2, f3, f4, f6, f7});
+
+    auto* src = R"(
+%f1 = func():void {
+  $B1: {
+    ret
+  }
+}
+%f2 = func():void {
+  $B2: {
+    ret
+  }
+}
+%f3 = func():void {
+  $B3: {
+    ret
+  }
+}
+%f4 = func():void {
+  $B4: {
+    ret
+  }
+}
+%f5 = func():void {
+  $B5: {
+    ret
+  }
+}
+%f6 = func():void {
+  $B6: {
+    ret
+  }
+}
+%f7 = func():void {
+  $B7: {
+    ret
+  }
+}
+%foo = @fragment func():void {
+  $B8: {
+    %9:void = call %f1
+    %10:void = call %f5
+    ret
+  }
+}
+%bar = @fragment func():void {
+  $B9: {
+    %12:void = call %f2
+    %13:void = call %f3
+    %14:void = call %f4
+    %15:void = call %f6
+    %16:void = call %f7
+    ret
+  }
+}
+)";
+
+    auto* expect = R"(
+%f1 = func():void {
+  $B1: {
+    ret
+  }
+}
+%f5 = func():void {
+  $B2: {
+    ret
+  }
+}
+%foo = @fragment func():void {
+  $B3: {
+    %4:void = call %f1
+    %5:void = call %f5
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    Run(SingleEntryPoint, "foo");
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace
+}  // namespace tint::core::ir::transform