[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