transform: Add LoopToForLoop

A Transform that attempts to convert WGSL `loop {}` statements into a for-loop statement.
For-loops cause less broken behavior with FXC than our current loop constructs.

Bug: tint:952
Change-Id: I0b7b1dbec9df4c004b0419d3feae53a195ec79bb
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/56767
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: David Neto <dneto@google.com>
diff --git a/src/BUILD.gn b/src/BUILD.gn
index 9fb6ab9..08ff4d0 100644
--- a/src/BUILD.gn
+++ b/src/BUILD.gn
@@ -572,6 +572,8 @@
     "transform/fold_trivial_single_use_lets.h",
     "transform/inline_pointer_lets.cc",
     "transform/inline_pointer_lets.h",
+    "transform/loop_to_for_loop.cc",
+    "transform/loop_to_for_loop.h",
     "transform/manager.cc",
     "transform/manager.h",
     "transform/pad_array_elements.cc",
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index de1abeb..5ef7d5c 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -297,6 +297,8 @@
   transform/fold_trivial_single_use_lets.h
   transform/inline_pointer_lets.cc
   transform/inline_pointer_lets.h
+  transform/loop_to_for_loop.cc
+  transform/loop_to_for_loop.h
   transform/manager.cc
   transform/manager.h
   transform/pad_array_elements.cc
@@ -879,6 +881,7 @@
       transform/fold_constants_test.cc
       transform/fold_trivial_single_use_lets_test.cc
       transform/inline_pointer_lets_test.cc
+      transform/loop_to_for_loop_test.cc
       transform/pad_array_elements_test.cc
       transform/promote_initializers_to_const_var_test.cc
       transform/renamer_test.cc
diff --git a/src/transform/decompose_memory_access.cc b/src/transform/decompose_memory_access.cc
index 7be526a..a1612e6 100644
--- a/src/transform/decompose_memory_access.cc
+++ b/src/transform/decompose_memory_access.cc
@@ -215,7 +215,7 @@
   return 4;
 }
 
-/// @returns the numer of bytes between columns of the given matrix
+/// @returns the number of bytes between columns of the given matrix
 uint32_t MatrixColumnStride(const sem::Matrix* mat) {
   return ScalarSize(mat->type()) * ((mat->rows() == 2) ? 2 : 4);
 }
diff --git a/src/transform/loop_to_for_loop.cc b/src/transform/loop_to_for_loop.cc
new file mode 100644
index 0000000..63c6314
--- /dev/null
+++ b/src/transform/loop_to_for_loop.cc
@@ -0,0 +1,133 @@
+// Copyright 2021 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/transform/loop_to_for_loop.h"
+
+#include "src/ast/break_statement.h"
+#include "src/ast/for_loop_statement.h"
+#include "src/program_builder.h"
+#include "src/sem/block_statement.h"
+#include "src/sem/function.h"
+#include "src/sem/statement.h"
+#include "src/sem/variable.h"
+#include "src/utils/scoped_assignment.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::transform::LoopToForLoop);
+
+namespace tint {
+namespace transform {
+namespace {
+
+bool IsBlockWithSingleBreak(ast::BlockStatement* block) {
+  if (block->size() != 1) {
+    return false;
+  }
+  return block->statements()[0]->Is<ast::BreakStatement>();
+}
+
+bool IsVarUsedByStmt(const sem::Info& sem,
+                     ast::Variable* var,
+                     ast::Statement* stmt) {
+  auto* var_sem = sem.Get(var);
+  for (auto* user : var_sem->Users()) {
+    if (auto* s = user->Stmt()) {
+      if (s->Declaration() == stmt) {
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
+}  // namespace
+
+LoopToForLoop::LoopToForLoop() = default;
+
+LoopToForLoop::~LoopToForLoop() = default;
+
+void LoopToForLoop::Run(CloneContext& ctx, const DataMap&, DataMap&) {
+  ctx.ReplaceAll([&](ast::LoopStatement* loop) -> ast::Statement* {
+    // For loop condition is taken from the first statement in the loop.
+    // This requires an if-statement with either:
+    //  * A true block with no else statements, and the true block contains a
+    //    single 'break' statement.
+    //  * An empty true block with a single, no-condition else statement
+    //    containing a single 'break' statement.
+    // Examples:
+    //   loop {  if (condition) { break; } ... }
+    //   loop {  if (condition) {} else { break; } ... }
+    auto& stmts = loop->body()->statements();
+    if (stmts.empty()) {
+      return nullptr;
+    }
+    auto* if_stmt = stmts[0]->As<ast::IfStatement>();
+    if (!if_stmt) {
+      return nullptr;
+    }
+
+    bool negate_condition = false;
+    if (IsBlockWithSingleBreak(if_stmt->body()) &&
+        if_stmt->else_statements().empty()) {
+      negate_condition = true;
+    } else if (if_stmt->body()->empty() &&
+               if_stmt->else_statements().size() == 1 &&
+               if_stmt->else_statements()[0]->condition() == nullptr &&
+               IsBlockWithSingleBreak(if_stmt->else_statements()[0]->body())) {
+      negate_condition = false;
+    } else {
+      return nullptr;
+    }
+
+    // The continuing block must be empty or contain a single statement
+    ast::Statement* continuing = nullptr;
+    if (auto* loop_cont = loop->continuing()) {
+      if (loop_cont->statements().size() != 1) {
+        return nullptr;
+      }
+
+      continuing = loop_cont->statements()[0];
+
+      // And the continuing statement must not use any of the variables declared
+      // in the loop body.
+      for (auto* stmt : loop->body()->statements()) {
+        if (auto* var_decl = stmt->As<ast::VariableDeclStatement>()) {
+          if (IsVarUsedByStmt(ctx.src->Sem(), var_decl->variable(),
+                              continuing)) {
+            return nullptr;
+          }
+        }
+      }
+
+      continuing = ctx.Clone(continuing);
+    }
+
+    auto* condition = ctx.Clone(if_stmt->condition());
+    if (negate_condition) {
+      condition = ctx.dst->create<ast::UnaryOpExpression>(ast::UnaryOp::kNot,
+                                                          condition);
+    }
+
+    ast::Statement* initializer = nullptr;
+
+    ctx.Remove(loop->body()->statements(), if_stmt);
+    auto* body = ctx.Clone(loop->body());
+    return ctx.dst->create<ast::ForLoopStatement>(initializer, condition,
+                                                  continuing, body);
+  });
+
+  ctx.Clone();
+}
+
+}  // namespace transform
+}  // namespace tint
diff --git a/src/transform/loop_to_for_loop.h b/src/transform/loop_to_for_loop.h
new file mode 100644
index 0000000..a227d70
--- /dev/null
+++ b/src/transform/loop_to_for_loop.h
@@ -0,0 +1,49 @@
+// Copyright 2021 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_TRANSFORM_LOOP_TO_FOR_LOOP_H_
+#define SRC_TRANSFORM_LOOP_TO_FOR_LOOP_H_
+
+#include <string>
+#include <unordered_map>
+
+#include "src/transform/transform.h"
+
+namespace tint {
+namespace transform {
+
+/// LoopToForLoop is a Transform that attempts to convert WGSL `loop {}`
+/// statements into a for-loop statement.
+class LoopToForLoop : public Castable<LoopToForLoop, Transform> {
+ public:
+  /// Constructor
+  LoopToForLoop();
+
+  /// Destructor
+  ~LoopToForLoop() override;
+
+ protected:
+  /// Runs the transform using the CloneContext built for transforming a
+  /// program. Run() is responsible for calling Clone() on the CloneContext.
+  /// @param ctx the CloneContext primed with the input program and
+  /// ProgramBuilder
+  /// @param inputs optional extra transform-specific input data
+  /// @param outputs optional extra transform-specific output data
+  void Run(CloneContext& ctx, const DataMap& inputs, DataMap& outputs) override;
+};
+
+}  // namespace transform
+}  // namespace tint
+
+#endif  // SRC_TRANSFORM_LOOP_TO_FOR_LOOP_H_
diff --git a/src/transform/loop_to_for_loop_test.cc b/src/transform/loop_to_for_loop_test.cc
new file mode 100644
index 0000000..91f2ffd
--- /dev/null
+++ b/src/transform/loop_to_for_loop_test.cc
@@ -0,0 +1,264 @@
+// Copyright 2021 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/transform/loop_to_for_loop.h"
+
+#include "src/transform/test_helper.h"
+
+namespace tint {
+namespace transform {
+namespace {
+
+using LoopToForLoopTest = TransformTest;
+
+TEST_F(LoopToForLoopTest, EmptyModule) {
+  auto* src = "";
+  auto* expect = "";
+
+  auto got = Run<LoopToForLoop>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(LoopToForLoopTest, IfBreak) {
+  auto* src = R"(
+fn f() {
+  var i : i32;
+  i = 0;
+  loop {
+    if (i > 15) {
+      break;
+    }
+
+    ignore(123);
+
+    continuing {
+      i = i + 1;
+    }
+  }
+}
+)";
+
+  auto* expect = R"(
+fn f() {
+  var i : i32;
+  i = 0;
+  for(; !((i > 15)); i = (i + 1)) {
+    ignore(123);
+  }
+}
+)";
+
+  auto got = Run<LoopToForLoop>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(LoopToForLoopTest, IfElseBreak) {
+  auto* src = R"(
+fn f() {
+  var i : i32;
+  i = 0;
+  loop {
+    if (i < 15) {
+    } else {
+      break;
+    }
+
+    ignore(123);
+
+    continuing {
+      i = i + 1;
+    }
+  }
+}
+)";
+
+  auto* expect = R"(
+fn f() {
+  var i : i32;
+  i = 0;
+  for(; (i < 15); i = (i + 1)) {
+    ignore(123);
+  }
+}
+)";
+
+  auto got = Run<LoopToForLoop>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(LoopToForLoopTest, Nested) {
+  auto* src = R"(
+let N = 16u;
+
+fn f() {
+  var i : u32 = 0u;
+  loop {
+    if (i >= N) {
+      break;
+    }
+    {
+      var j : u32 = 0u;
+      loop {
+        if (j >= N) {
+          break;
+        }
+
+        ignore(i);
+        ignore(j);
+
+        continuing {
+          j = (j + 1u);
+        }
+      }
+    }
+
+    continuing {
+      i = (i + 1u);
+    }
+  }
+}
+)";
+
+  auto* expect = R"(
+let N = 16u;
+
+fn f() {
+  var i : u32 = 0u;
+  for(; !((i >= N)); i = (i + 1u)) {
+    {
+      var j : u32 = 0u;
+      for(; !((j >= N)); j = (j + 1u)) {
+        ignore(i);
+        ignore(j);
+      }
+    }
+  }
+}
+)";
+
+  auto got = Run<LoopToForLoop>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(LoopToForLoopTest, NoTransform_IfMultipleStmts) {
+  auto* src = R"(
+fn f() {
+  var i : i32;
+  i = 0;
+  loop {
+    if ((i < 15)) {
+      ignore(i);
+      break;
+    }
+    ignore(123);
+
+    continuing {
+      i = (i + 1);
+    }
+  }
+}
+)";
+
+  auto* expect = src;
+
+  auto got = Run<LoopToForLoop>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(LoopToForLoopTest, NoTransform_IfElseMultipleStmts) {
+  auto* src = R"(
+fn f() {
+  var i : i32;
+  i = 0;
+  loop {
+    if ((i < 15)) {
+    } else {
+      ignore(i);
+      break;
+    }
+    ignore(123);
+
+    continuing {
+      i = (i + 1);
+    }
+  }
+}
+)";
+
+  auto* expect = src;
+
+  auto got = Run<LoopToForLoop>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(LoopToForLoopTest, NoTransform_ContinuingMultipleStmts) {
+  auto* src = R"(
+fn f() {
+  var i : i32;
+  i = 0;
+  loop {
+    if ((i < 15)) {
+      break;
+    }
+    ignore(123);
+
+    continuing {
+      i = (i + 1);
+      ignore(i);
+    }
+  }
+}
+)";
+
+  auto* expect = src;
+
+  auto got = Run<LoopToForLoop>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(LoopToForLoopTest, NoTransform_ContinuingUsesVarDeclInLoopBody) {
+  auto* src = R"(
+fn f() {
+  var i : i32;
+  i = 0;
+  loop {
+    if ((i < 15)) {
+      break;
+    }
+    var j : i32;
+
+    continuing {
+      i = (i + j);
+    }
+  }
+}
+)";
+
+  auto* expect = src;
+
+  auto got = Run<LoopToForLoop>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+}  // namespace
+}  // namespace transform
+}  // namespace tint
diff --git a/test/BUILD.gn b/test/BUILD.gn
index 87941ac..99f0e23 100644
--- a/test/BUILD.gn
+++ b/test/BUILD.gn
@@ -287,6 +287,7 @@
     "../src/transform/fold_constants_test.cc",
     "../src/transform/fold_trivial_single_use_lets_test.cc",
     "../src/transform/inline_pointer_lets_test.cc",
+    "../src/transform/loop_to_for_loop_test.cc",
     "../src/transform/pad_array_elements_test.cc",
     "../src/transform/promote_initializers_to_const_var_test.cc",
     "../src/transform/renamer_test.cc",