In runtime effects, verify that loops conform to ES2 rules

Bug: skia:11094
Change-Id: I68a08e79d29579901b74daca3c22f5112fbb3c8c
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/353356
Commit-Queue: Brian Osman <brianosman@google.com>
Reviewed-by: Mike Klein <mtklein@google.com>
diff --git a/gn/sksl_tests.gni b/gn/sksl_tests.gni
index 33565279..f45d06c 100644
--- a/gn/sksl_tests.gni
+++ b/gn/sksl_tests.gni
@@ -496,6 +496,10 @@
   "$_tests/sksl/errors/IllegalOperators.rte",
   "$_tests/sksl/errors/UnsupportedTypeSampler.rte",
   "$_tests/sksl/errors/UnsupportedTypeTexture.rte",
+  "$_tests/sksl/runtime_errors/LoopConditionErrors.rte",
+  "$_tests/sksl/runtime_errors/LoopExpressionErrors.rte",
+  "$_tests/sksl/runtime_errors/LoopInitializerErrors.rte",
+  "$_tests/sksl/runtime_errors/LoopStructureErrors.rte",
 ]
 
 # Tests in sksl_fp_tests_sources will be compiled with --settings on, and are expected to generate
diff --git a/src/sksl/SkSLAnalysis.cpp b/src/sksl/SkSLAnalysis.cpp
index 3d5ef22..2462ec3 100644
--- a/src/sksl/SkSLAnalysis.cpp
+++ b/src/sksl/SkSLAnalysis.cpp
@@ -460,6 +460,191 @@
             IsTrivialExpression(*expr.as<IndexExpression>().base()));
 }
 
+static const char* invalid_for_ES2(const ForStatement& loop,
+                                   Analysis::UnrollableLoopInfo& loopInfo) {
+    auto getConstant = [&](const std::unique_ptr<Expression>& expr, double* val) {
+        if (!expr->isCompileTimeConstant()) {
+            return false;
+        }
+        if (!expr->type().isInteger() && !expr->type().isFloat()) {
+            SkDEBUGFAIL("unexpected constant type");
+            return false;
+        }
+
+        *val = expr->type().isInteger() ? static_cast<double>(expr->getConstantInt())
+                                        : static_cast<double>(expr->getConstantFloat());
+        return true;
+    };
+
+    //
+    // init_declaration has the form: type_specifier identifier = constant_expression
+    //
+    if (!loop.initializer()) {
+        return "missing init declaration";
+    }
+    if (!loop.initializer()->is<VarDeclaration>()) {
+        return "invalid init declaration";
+    }
+    const VarDeclaration& initDecl = loop.initializer()->as<VarDeclaration>();
+    if (!initDecl.baseType().isInteger() && !initDecl.baseType().isFloat()) {
+        return "invalid type for loop index";
+    }
+    if (initDecl.arraySize() != 0) {
+        return "invalid type for loop index";
+    }
+    if (!initDecl.value()) {
+        return "missing loop index initializer";
+    }
+    if (!getConstant(initDecl.value(), &loopInfo.fStart)) {
+        return "loop index initializer must be a constant expression";
+    }
+
+    loopInfo.fIndex = &initDecl.var();
+
+    auto is_loop_index = [&](const std::unique_ptr<Expression>& expr) {
+        return expr->is<VariableReference>() &&
+               expr->as<VariableReference>().variable() == loopInfo.fIndex;
+    };
+
+    //
+    // condition has the form: loop_index relational_operator constant_expression
+    //
+    if (!loop.test()) {
+        return "missing condition";
+    }
+    if (!loop.test()->is<BinaryExpression>()) {
+        return "invalid condition";
+    }
+    const BinaryExpression& cond = loop.test()->as<BinaryExpression>();
+    if (!is_loop_index(cond.left())) {
+        return "expected loop index on left hand side of condition";
+    }
+    // relational_operator is one of: > >= < <= == or !=
+    switch (cond.getOperator()) {
+        case Token::Kind::TK_GT:
+        case Token::Kind::TK_GTEQ:
+        case Token::Kind::TK_LT:
+        case Token::Kind::TK_LTEQ:
+        case Token::Kind::TK_EQEQ:
+        case Token::Kind::TK_NEQ:
+            break;
+        default:
+            return "invalid relational operator";
+    }
+    double loopEnd = 0;
+    if (!getConstant(cond.right(), &loopEnd)) {
+        return "loop index must be compared with a constant expression";
+    }
+
+    //
+    // expression has one of the following forms:
+    //   loop_index++
+    //   loop_index--
+    //   loop_index += constant_expression
+    //   loop_index -= constant_expression
+    // The spec doesn't mention prefix increment and decrement, but there is some consensus that
+    // it's an oversight, so we allow those as well.
+    //
+    if (!loop.next()) {
+        return "missing loop expression";
+    }
+    switch (loop.next()->kind()) {
+        case Expression::Kind::kBinary: {
+            const BinaryExpression& next = loop.next()->as<BinaryExpression>();
+            if (!is_loop_index(next.left())) {
+                return "expected loop index in loop expression";
+            }
+            if (!getConstant(next.right(), &loopInfo.fDelta)) {
+                return "loop index must be modified by a constant expression";
+            }
+            switch (next.getOperator()) {
+                case Token::Kind::TK_PLUSEQ:                                      break;
+                case Token::Kind::TK_MINUSEQ: loopInfo.fDelta = -loopInfo.fDelta; break;
+                default:
+                    return "invalid operator in loop expression";
+            }
+        } break;
+        case Expression::Kind::kPrefix: {
+            const PrefixExpression& next = loop.next()->as<PrefixExpression>();
+            if (!is_loop_index(next.operand())) {
+                return "expected loop index in loop expression";
+            }
+            switch (next.getOperator()) {
+                case Token::Kind::TK_PLUSPLUS:   loopInfo.fDelta =  1; break;
+                case Token::Kind::TK_MINUSMINUS: loopInfo.fDelta = -1; break;
+                default:
+                    return "invalid operator in loop expression";
+            }
+        } break;
+        case Expression::Kind::kPostfix: {
+            const PostfixExpression& next = loop.next()->as<PostfixExpression>();
+            if (!is_loop_index(next.operand())) {
+                return "expected loop index in loop expression";
+            }
+            switch (next.getOperator()) {
+                case Token::Kind::TK_PLUSPLUS:   loopInfo.fDelta =  1; break;
+                case Token::Kind::TK_MINUSMINUS: loopInfo.fDelta = -1; break;
+                default:
+                    return "invalid operator in loop expression";
+            }
+        } break;
+        default:
+            return "invalid loop expression";
+    }
+
+    //
+    // Within the body of the loop, the loop index is not statically assigned to, nor is it used as
+    // argument to a function 'out' or 'inout' parameter.
+    //
+    if (Analysis::StatementWritesToVariable(*loop.statement(), initDecl.var())) {
+        return "loop index must not be modified within body of the loop";
+    }
+
+    // Finally, compute the iteration count, based on the bounds, and the termination operator.
+    constexpr int kMaxUnrollableLoopLength = 128;
+    loopInfo.fCount = 0;
+
+    double val = loopInfo.fStart;
+    auto evalCond = [&]() {
+        switch (cond.getOperator()) {
+            case Token::Kind::TK_GT:   return val >  loopEnd;
+            case Token::Kind::TK_GTEQ: return val >= loopEnd;
+            case Token::Kind::TK_LT:   return val <  loopEnd;
+            case Token::Kind::TK_LTEQ: return val <= loopEnd;
+            case Token::Kind::TK_EQEQ: return val == loopEnd;
+            case Token::Kind::TK_NEQ:  return val != loopEnd;
+            default: SkUNREACHABLE;
+        }
+    };
+
+    for (loopInfo.fCount = 0; loopInfo.fCount <= kMaxUnrollableLoopLength; ++loopInfo.fCount) {
+        if (!evalCond()) {
+            break;
+        }
+        val += loopInfo.fDelta;
+    }
+
+    if (loopInfo.fCount > kMaxUnrollableLoopLength) {
+        return "loop must guarantee termination in fewer iterations";
+    }
+
+    return nullptr;  // All checks pass
+}
+
+bool Analysis::ForLoopIsValidForES2(const ForStatement& loop,
+                                    Analysis::UnrollableLoopInfo* outLoopInfo,
+                                    ErrorReporter* errors) {
+    UnrollableLoopInfo ignored,
+                       *loopInfo = outLoopInfo ? outLoopInfo : &ignored;
+    if (const char* msg = invalid_for_ES2(loop, *loopInfo)) {
+        if (errors) {
+            errors->error(loop.fOffset, msg);
+        }
+        return false;
+    }
+    return true;
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // ProgramVisitor
 
diff --git a/src/sksl/SkSLAnalysis.h b/src/sksl/SkSLAnalysis.h
index c853964..43883e1 100644
--- a/src/sksl/SkSLAnalysis.h
+++ b/src/sksl/SkSLAnalysis.h
@@ -17,6 +17,7 @@
 
 class ErrorReporter;
 class Expression;
+class ForStatement;
 class FunctionDeclaration;
 class FunctionDefinition;
 struct LoadedModule;
@@ -66,6 +67,21 @@
     // - half4(myColor.a)
     // - myStruct.myArrayField[7].xyz
     static bool IsTrivialExpression(const Expression& expr);
+
+    struct UnrollableLoopInfo {
+        const Variable* fIndex;
+        double fStart;
+        double fDelta;
+        int fCount;
+    };
+
+    // Ensures that 'loop' meets the strict requirements of The OpenGL ES Shading Language 1.00,
+    // Appendix A, Section 4.
+    // Information about the loop's structure are placed in outLoopInfo (if not nullptr).
+    // If the function returns false, specific reasons are reported via errors (if not nullptr).
+    static bool ForLoopIsValidForES2(const ForStatement& loop,
+                                     UnrollableLoopInfo* outLoopInfo,
+                                     ErrorReporter* errors);
 };
 
 /**
diff --git a/src/sksl/SkSLIRGenerator.cpp b/src/sksl/SkSLIRGenerator.cpp
index 1e3e2bc..75ca4e8 100644
--- a/src/sksl/SkSLIRGenerator.cpp
+++ b/src/sksl/SkSLIRGenerator.cpp
@@ -568,8 +568,17 @@
     if (!statement) {
         return nullptr;
     }
-    return std::make_unique<ForStatement>(f.fOffset, std::move(initializer), std::move(test),
-                                          std::move(next), std::move(statement), fSymbolTable);
+
+    auto forStmt =
+            std::make_unique<ForStatement>(f.fOffset, std::move(initializer), std::move(test),
+                                           std::move(next), std::move(statement), fSymbolTable);
+    if (this->strictES2Mode()) {
+        if (!Analysis::ForLoopIsValidForES2(*forStmt, /*outLoopInfo=*/nullptr,
+                                            &this->errorReporter())) {
+            return nullptr;
+        }
+    }
+    return std::move(forStmt);
 }
 
 std::unique_ptr<Statement> IRGenerator::convertWhile(int offset, std::unique_ptr<Expression> test,
diff --git a/tests/SkSLInterpreterTest.cpp b/tests/SkSLInterpreterTest.cpp
index 92622d22d..3292ee3 100644
--- a/tests/SkSLInterpreterTest.cpp
+++ b/tests/SkSLInterpreterTest.cpp
@@ -508,8 +508,8 @@
     test(r,
          "void main(inout half4 color) {"
          "    for (int i = 1; i <= 10; ++i)"
-         "        for (int j = i; j <= 10; ++j)"
-         "            color.r += j;"
+         "        for (int j = 1; j <= 10; ++j)"
+         "            if (j >= i) { color.r += j; }"
          "}",
          0, 0, 0, 0,
          385, 0, 0, 0,
@@ -517,7 +517,7 @@
     test(r,
          "void main(inout half4 color) {"
          "    for (int i = 1; i <= 10; ++i)"
-         "        for (int j = 1; ; ++j) {"
+         "        for (int j = 1; j < 20 ; ++j) {"
          "            if (i == j) continue;"
          "            if (j > 10) break;"
          "            color.r += j;"
@@ -602,7 +602,8 @@
         // Kitchen sink (swizzles, inout, SoAoS)
         "struct ManyRects { int numRects; RectAndColor rects[4]; };\n"
         "void fill_rects(inout ManyRects mr) {\n"
-        "  for (int i = 0; i < mr.numRects; ++i) {\n"
+        "  for (int i = 0; i < 4; ++i) {\n"
+        "    if (i >= mr.numRects) { break; }\n"
         "    mr.rects[i].r = gRects[i];\n"
         "    float b = mr.rects[i].r.p1.y;\n"
         "    mr.rects[i].color = float4(b, b, b, b);\n"
diff --git a/tests/sksl/runtime_errors/LoopConditionErrors.rte b/tests/sksl/runtime_errors/LoopConditionErrors.rte
new file mode 100644
index 0000000..d9109ff
--- /dev/null
+++ b/tests/sksl/runtime_errors/LoopConditionErrors.rte
@@ -0,0 +1,16 @@
+// Expect 9 errors
+
+void no_condition() { for (int i = 0;;i++) {} }
+
+void unary_cond_op()   { for (int i = 0; !(i > 1); ++i) {} }
+void implict_cond_op() { for (int i = 1; bool(i); --i) {} }
+void complex_cond_op() { for (int i = 0; i < 1 && i < 2; ++i) {} }
+
+void cond_wrong_var()  { int j = 0; for (int i = 0; j < 1; ++i) {} }
+void cond_wrong_side() { for (int i = 0; 1 > i; ++i) {} }
+void cond_index_cast() { for (int i = 0; float(i) < 1.5; ++i) {} }
+
+uniform int u;
+
+void cond_uniform_val()    { for (int i = 0; i < u; ++i) {} }
+void cond_param_val(int p) { for (int i = 0; i < p; ++i) {} }
diff --git a/tests/sksl/runtime_errors/LoopExpressionErrors.rte b/tests/sksl/runtime_errors/LoopExpressionErrors.rte
new file mode 100644
index 0000000..a161a16
--- /dev/null
+++ b/tests/sksl/runtime_errors/LoopExpressionErrors.rte
@@ -0,0 +1,13 @@
+// Expect 7 errors
+
+void no_expression() { for (int i = 0; i < 1;) {} }
+
+void expression_equals()     { for (int i = 0; i < 1; i = 1) {} }
+void expression_equal_plus() { for (int i = 0; i < 1; i = i + 1) {} }
+void expression_times_eq()   { for (int i = 1; i < 2; i *= 2) {} }
+void expression_bad_unary()  { for (int i = 0; i < 1; -i) {} }
+
+uniform int u;
+
+void expression_uniform_val()    { for (int i = 0; i < 1; i += u) {} }
+void expression_param_val(int p) { for (int i = 0; i < 1; i += p) {} }
diff --git a/tests/sksl/runtime_errors/LoopInitializerErrors.rte b/tests/sksl/runtime_errors/LoopInitializerErrors.rte
new file mode 100644
index 0000000..974a48f
--- /dev/null
+++ b/tests/sksl/runtime_errors/LoopInitializerErrors.rte
@@ -0,0 +1,14 @@
+// Expect 8 errors
+
+void no_initializer() { int i = 0; for (     ; i < 1; i++) {} }
+void init_not_decl()  { int i;     for (i = 0; i < 1; i++) {} }
+void index_no_init()  { for (int i; i < 1; i++) {} }
+
+void bool_index()  { for (bool i = false; i != true; i = !i) {} }
+void vec_index()   { for (float2 i = float2(0); i.x < 1; i.x++) {} }
+void array_index() { for (int i[2] = int[2](0, 0); i[0] < 1; ++i[0]) {} }
+
+uniform int u;
+
+void uniform_init()    { for (int i = u; i < 1; ++i) {} }
+void param_init(int p) { for (int i = p; i < 1; ++i) {} }
diff --git a/tests/sksl/runtime_errors/LoopStructureErrors.rte b/tests/sksl/runtime_errors/LoopStructureErrors.rte
new file mode 100644
index 0000000..48a5c60
--- /dev/null
+++ b/tests/sksl/runtime_errors/LoopStructureErrors.rte
@@ -0,0 +1,12 @@
+// Expect 5 errors
+
+void loop_length_ok() { for (int i = 0; i < 128; i++) {} }  // LEGAL: See kMaxUnrollableLoopLength
+void loop_too_long()  { for (int i = 0; i < 129; i++) {} }
+void infinite_loop()  { for (int i = 0; i < 1; i += 0) {} }
+
+void set(out int x)   { x = 1; }
+void inc(inout int x) { x++; }
+
+void index_modified()    { for (int i = 0; i < 2; i++) { i++; } }
+void index_out_param()   { for (int i = 0; i < 1; i++) { set(i); } }
+void index_inout_param() { for (int i = 0; i < 1; i++) { inc(i); } }
diff --git a/tests/sksl/runtime_errors/golden/LoopConditionErrors.skvm b/tests/sksl/runtime_errors/golden/LoopConditionErrors.skvm
new file mode 100644
index 0000000..bf5e9b4
--- /dev/null
+++ b/tests/sksl/runtime_errors/golden/LoopConditionErrors.skvm
@@ -0,0 +1,12 @@
+### Compilation failed:
+
+error: 3: missing condition
+error: 5: invalid condition
+error: 6: invalid condition
+error: 7: expected loop index on left hand side of condition
+error: 9: expected loop index on left hand side of condition
+error: 10: expected loop index on left hand side of condition
+error: 11: expected loop index on left hand side of condition
+error: 15: loop index must be compared with a constant expression
+error: 16: loop index must be compared with a constant expression
+9 errors
diff --git a/tests/sksl/runtime_errors/golden/LoopExpressionErrors.skvm b/tests/sksl/runtime_errors/golden/LoopExpressionErrors.skvm
new file mode 100644
index 0000000..d0e92e1
--- /dev/null
+++ b/tests/sksl/runtime_errors/golden/LoopExpressionErrors.skvm
@@ -0,0 +1,10 @@
+### Compilation failed:
+
+error: 3: missing loop expression
+error: 5: invalid operator in loop expression
+error: 6: loop index must be modified by a constant expression
+error: 7: invalid operator in loop expression
+error: 8: invalid operator in loop expression
+error: 12: loop index must be modified by a constant expression
+error: 13: loop index must be modified by a constant expression
+7 errors
diff --git a/tests/sksl/runtime_errors/golden/LoopInitializerErrors.skvm b/tests/sksl/runtime_errors/golden/LoopInitializerErrors.skvm
new file mode 100644
index 0000000..4326716
--- /dev/null
+++ b/tests/sksl/runtime_errors/golden/LoopInitializerErrors.skvm
@@ -0,0 +1,11 @@
+### Compilation failed:
+
+error: 3: missing init declaration
+error: 4: invalid init declaration
+error: 5: missing loop index initializer
+error: 7: invalid type for loop index
+error: 8: invalid type for loop index
+error: 9: invalid type for loop index
+error: 13: loop index initializer must be a constant expression
+error: 14: loop index initializer must be a constant expression
+8 errors
diff --git a/tests/sksl/runtime_errors/golden/LoopStructureErrors.skvm b/tests/sksl/runtime_errors/golden/LoopStructureErrors.skvm
new file mode 100644
index 0000000..851758a
--- /dev/null
+++ b/tests/sksl/runtime_errors/golden/LoopStructureErrors.skvm
@@ -0,0 +1,8 @@
+### Compilation failed:
+
+error: 4: loop must guarantee termination in fewer iterations
+error: 5: loop must guarantee termination in fewer iterations
+error: 10: loop index must not be modified within body of the loop
+error: 11: loop index must not be modified within body of the loop
+error: 12: loop index must not be modified within body of the loop
+5 errors