[ir][spirv-writer] Add BindingRemapper transform

Just remap binding points for now, leaving access mode remapping as
unimplemented.

Bug: tint:1906
Change-Id: If16909584ac37da5539a1a07e69f0b383f38f92f
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/143425
Auto-Submit: James Price <jrprice@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: James Price <jrprice@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 60e8ad2..f60cd8f 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -493,6 +493,8 @@
     sources = [
       "lang/core/ir/transform/add_empty_entry_point.cc",
       "lang/core/ir/transform/add_empty_entry_point.h",
+      "lang/core/ir/transform/binding_remapper.cc",
+      "lang/core/ir/transform/binding_remapper.h",
       "lang/core/ir/transform/block_decorated_structs.cc",
       "lang/core/ir/transform/block_decorated_structs.h",
       "lang/core/ir/transform/demote_to_helper.cc",
@@ -1885,6 +1887,7 @@
     tint_unittests_source_set("tint_unittests_ir_transform_src") {
       sources = [
         "lang/core/ir/transform/add_empty_entry_point_test.cc",
+        "lang/core/ir/transform/binding_remapper_test.cc",
         "lang/core/ir/transform/block_decorated_structs_test.cc",
         "lang/core/ir/transform/demote_to_helper_test.cc",
         "lang/core/ir/transform/helper_test.h",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 84e30c8..8dc1023 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -872,6 +872,8 @@
     lang/core/ir/var.h
     lang/core/ir/transform/add_empty_entry_point.cc
     lang/core/ir/transform/add_empty_entry_point.h
+    lang/core/ir/transform/binding_remapper.cc
+    lang/core/ir/transform/binding_remapper.h
     lang/core/ir/transform/block_decorated_structs.cc
     lang/core/ir/transform/block_decorated_structs.h
     lang/core/ir/transform/demote_to_helper.cc
@@ -1671,6 +1673,7 @@
       lang/core/ir/switch_test.cc
       lang/core/ir/swizzle_test.cc
       lang/core/ir/transform/add_empty_entry_point_test.cc
+      lang/core/ir/transform/binding_remapper_test.cc
       lang/core/ir/transform/block_decorated_structs_test.cc
       lang/core/ir/transform/demote_to_helper_test.cc
       lang/core/ir/unary_test.cc
diff --git a/src/tint/lang/core/ir/transform/binding_remapper.cc b/src/tint/lang/core/ir/transform/binding_remapper.cc
new file mode 100644
index 0000000..04217bf
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/binding_remapper.cc
@@ -0,0 +1,77 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/core/ir/transform/binding_remapper.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/module.h"
+#include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/core/ir/var.h"
+#include "src/tint/utils/result/result.h"
+#include "src/tint/utils/text/string.h"
+
+using namespace tint::number_suffixes;  // NOLINT
+
+namespace tint::ir::transform {
+
+namespace {
+
+Result<SuccessType, std::string> Run(ir::Module* ir, const BindingRemapperOptions& options) {
+    if (!options.access_controls.empty()) {
+        return std::string("remapping access controls is currently unsupported");
+    }
+    if (options.binding_points.empty()) {
+        return Success;
+    }
+    if (!ir->root_block) {
+        return Success;
+    }
+
+    // Find binding resources.
+    for (auto inst : *ir->root_block) {
+        auto* var = inst->As<Var>();
+        if (!var || !var->Alive()) {
+            continue;
+        }
+
+        auto bp = var->BindingPoint();
+        if (!bp) {
+            continue;
+        }
+
+        // Replace group and binding index if requested.
+        tint::BindingPoint from{bp->group, bp->binding};
+        auto to = options.binding_points.find(from);
+        if (to != options.binding_points.end()) {
+            var->SetBindingPoint(to->second.group, to->second.binding);
+        }
+    }
+
+    return Success;
+}
+
+}  // namespace
+
+Result<SuccessType, std::string> BindingRemapper(Module* ir,
+                                                 const BindingRemapperOptions& options) {
+    auto result = ValidateAndDumpIfNeeded(*ir, "BindingRemapper transform");
+    if (!result) {
+        return result;
+    }
+
+    return Run(ir, options);
+}
+
+}  // namespace tint::ir::transform
diff --git a/src/tint/lang/core/ir/transform/binding_remapper.h b/src/tint/lang/core/ir/transform/binding_remapper.h
new file mode 100644
index 0000000..2c1fbe6
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/binding_remapper.h
@@ -0,0 +1,39 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_LANG_CORE_IR_TRANSFORM_BINDING_REMAPPER_H_
+#define SRC_TINT_LANG_CORE_IR_TRANSFORM_BINDING_REMAPPER_H_
+
+#include <string>
+
+#include "src/tint/utils/result/result.h"
+#include "tint/binding_remapper_options.h"
+
+// Forward declarations.
+namespace tint::ir {
+class Module;
+}
+
+namespace tint::ir::transform {
+
+/// BindingRemapper is a transform that remaps binding point indices and access controls.
+/// @param module the module to transform
+/// @param options the remapping options
+/// @returns an error string on failure
+Result<SuccessType, std::string> BindingRemapper(Module* module,
+                                                 const BindingRemapperOptions& options);
+
+}  // namespace tint::ir::transform
+
+#endif  // SRC_TINT_LANG_CORE_IR_TRANSFORM_BINDING_REMAPPER_H_
diff --git a/src/tint/lang/core/ir/transform/binding_remapper_test.cc b/src/tint/lang/core/ir/transform/binding_remapper_test.cc
new file mode 100644
index 0000000..9b02052
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/binding_remapper_test.cc
@@ -0,0 +1,241 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/core/ir/transform/binding_remapper.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+
+namespace tint::ir::transform {
+namespace {
+
+using namespace tint::builtin::fluent_types;  // NOLINT
+using namespace tint::number_suffixes;        // NOLINT
+
+using IR_BindingRemapperTest = TransformTest;
+
+TEST_F(IR_BindingRemapperTest, NoModify_NoRemappings) {
+    auto* buffer = b.Var("buffer", ty.ptr<uniform, i32>());
+    buffer->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(buffer);
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<uniform, i32, read_write> = var @binding_point(0, 0)
+}
+
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    BindingRemapperOptions options;
+    Run(BindingRemapper, options);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BindingRemapperTest, NoModify_RemappingDifferentBindingPoint) {
+    auto* buffer = b.Var("buffer", ty.ptr<uniform, i32>());
+    buffer->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(buffer);
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<uniform, i32, read_write> = var @binding_point(0, 0)
+}
+
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    BindingRemapperOptions options;
+    options.binding_points[tint::BindingPoint{0u, 1u}] = tint::BindingPoint{1u, 0u};
+    Run(BindingRemapper, options);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BindingRemapperTest, RemappingGroup) {
+    auto* buffer = b.Var("buffer", ty.ptr<uniform, i32>());
+    buffer->SetBindingPoint(1, 2);
+    b.RootBlock()->Append(buffer);
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<uniform, i32, read_write> = var @binding_point(1, 2)
+}
+
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %buffer:ptr<uniform, i32, read_write> = var @binding_point(3, 2)
+}
+
+)";
+
+    BindingRemapperOptions options;
+    options.binding_points[tint::BindingPoint{1u, 2u}] = tint::BindingPoint{3u, 2u};
+    Run(BindingRemapper, options);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BindingRemapperTest, RemappingBindingIndex) {
+    auto* buffer = b.Var("buffer", ty.ptr<uniform, i32>());
+    buffer->SetBindingPoint(1, 2);
+    b.RootBlock()->Append(buffer);
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<uniform, i32, read_write> = var @binding_point(1, 2)
+}
+
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %buffer:ptr<uniform, i32, read_write> = var @binding_point(1, 3)
+}
+
+)";
+
+    BindingRemapperOptions options;
+    options.binding_points[tint::BindingPoint{1u, 2u}] = tint::BindingPoint{1u, 3u};
+    Run(BindingRemapper, options);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BindingRemapperTest, RemappingGroupAndBindingIndex) {
+    auto* buffer = b.Var("buffer", ty.ptr<uniform, i32>());
+    buffer->SetBindingPoint(1, 2);
+    b.RootBlock()->Append(buffer);
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<uniform, i32, read_write> = var @binding_point(1, 2)
+}
+
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %buffer:ptr<uniform, i32, read_write> = var @binding_point(3, 4)
+}
+
+)";
+
+    BindingRemapperOptions options;
+    options.binding_points[tint::BindingPoint{1u, 2u}] = tint::BindingPoint{3u, 4u};
+    Run(BindingRemapper, options);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BindingRemapperTest, SwapTwoBindingPoints) {
+    auto* buffer_a = b.Var("buffer_a", ty.ptr<uniform, i32>());
+    buffer_a->SetBindingPoint(1, 2);
+    b.RootBlock()->Append(buffer_a);
+    auto* buffer_b = b.Var("buffer_b", ty.ptr<uniform, i32>());
+    buffer_b->SetBindingPoint(3, 4);
+    b.RootBlock()->Append(buffer_b);
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer_a:ptr<uniform, i32, read_write> = var @binding_point(1, 2)
+  %buffer_b:ptr<uniform, i32, read_write> = var @binding_point(3, 4)
+}
+
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %buffer_a:ptr<uniform, i32, read_write> = var @binding_point(3, 4)
+  %buffer_b:ptr<uniform, i32, read_write> = var @binding_point(1, 2)
+}
+
+)";
+
+    BindingRemapperOptions options;
+    options.binding_points[tint::BindingPoint{1u, 2u}] = tint::BindingPoint{3u, 4u};
+    options.binding_points[tint::BindingPoint{3u, 4u}] = tint::BindingPoint{1u, 2u};
+    Run(BindingRemapper, options);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BindingRemapperTest, BindingPointCollisionSameEntryPoint) {
+    auto* buffer_a = b.Var("buffer_a", ty.ptr<uniform, i32>());
+    buffer_a->SetBindingPoint(1, 2);
+    b.RootBlock()->Append(buffer_a);
+    auto* buffer_b = b.Var("buffer_b", ty.ptr<uniform, i32>());
+    buffer_b->SetBindingPoint(3, 4);
+    b.RootBlock()->Append(buffer_b);
+
+    auto* ep = b.Function("main", mod.Types().void_(), Function::PipelineStage::kFragment);
+    b.Append(ep->Block(), [&] {
+        b.Load(buffer_a);
+        b.Load(buffer_b);
+        b.Return(ep);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer_a:ptr<uniform, i32, read_write> = var @binding_point(1, 2)
+  %buffer_b:ptr<uniform, i32, read_write> = var @binding_point(3, 4)
+}
+
+%main = @fragment func():void -> %b2 {
+  %b2 = block {
+    %4:i32 = load %buffer_a
+    %5:i32 = load %buffer_b
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %buffer_a:ptr<uniform, i32, read_write> = var @binding_point(0, 1)
+  %buffer_b:ptr<uniform, i32, read_write> = var @binding_point(0, 1)
+}
+
+%main = @fragment func():void -> %b2 {
+  %b2 = block {
+    %4:i32 = load %buffer_a
+    %5:i32 = load %buffer_b
+    ret
+  }
+}
+)";
+
+    BindingRemapperOptions options;
+    options.binding_points[tint::BindingPoint{1u, 2u}] = tint::BindingPoint{0u, 1u};
+    options.binding_points[tint::BindingPoint{3u, 4u}] = tint::BindingPoint{0u, 1u};
+    Run(BindingRemapper, options);
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace
+}  // namespace tint::ir::transform
diff --git a/src/tint/lang/core/ir/transform/helper_test.h b/src/tint/lang/core/ir/transform/helper_test.h
index e055afb..8590171 100644
--- a/src/tint/lang/core/ir/transform/helper_test.h
+++ b/src/tint/lang/core/ir/transform/helper_test.h
@@ -54,9 +54,11 @@
 
     /// Transforms the module, using @p transform.
     /// @param transform_func the transform to run
-    void Run(std::function<Result<SuccessType, std::string>(Module*)> transform_func) {
+    /// @param args the arguments to the transform function
+    template <typename TRANSFORM, typename... ARGS>
+    void Run(TRANSFORM&& transform_func, ARGS&&... args) {
         // Run the transform.
-        auto result = transform_func(&mod);
+        auto result = transform_func(&mod, args...);
         EXPECT_TRUE(result) << result.Failure();
         if (!result) {
             return;
diff --git a/src/tint/lang/spirv/writer/writer.cc b/src/tint/lang/spirv/writer/writer.cc
index 4972769..1f364ce 100644
--- a/src/tint/lang/spirv/writer/writer.cc
+++ b/src/tint/lang/spirv/writer/writer.cc
@@ -19,6 +19,7 @@
 
 #include "src/tint/lang/spirv/writer/ast_printer/ast_printer.h"
 #if TINT_BUILD_IR
+#include "src/tint/lang/core/ir/transform/binding_remapper.h"
 #include "src/tint/lang/spirv/writer/printer/printer.h"             // nogncheck
 #include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"  // nogncheck
 #endif                                                              // TINT_BUILD_IR
@@ -46,8 +47,15 @@
             return "IR converter: " + converted.Failure();
         }
 
-        // Generate the SPIR-V code.
         auto ir = converted.Move();
+
+        // Apply transforms as required by writer options.
+        auto remapper = ir::transform::BindingRemapper(&ir, options.binding_remapper_options);
+        if (!remapper) {
+            return remapper.Failure();
+        }
+
+        // Generate the SPIR-V code.
         auto impl = std::make_unique<Printer>(&ir, zero_initialize_workgroup_memory);
         auto spirv = impl->Generate();
         if (!spirv) {