Demonstrate a bug with inlined static switches.

When we detect a static switch, the optimizer finds the matching switch-
case and eliminates all the other switch-cases. It handles case
fall-through by scanning forward and looking for an unconditional break.

However, the inliner has an interesting quirk--it can replace `return`
statements inside of a switch with `continue` statements, since the body
of the inlined function has been wrapped with a for-loop to allow for
early exits. The optimizer does not recognize these continue statements
as exits from the switch (although they certainly qualify), so it
treats continues as fallen-through and keeps emitting switch-cases.

The dead-code elimination pass was actually doing us a favor here and
eliminating the excess code later. A flag was added to disable DCE in
order to reveal the problem in a test.

Change-Id: I8ff19fde5e32d0ab73d7c5411da40cb953a446f5
Bug: skia:11352
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/372956
Auto-Submit: John Stiles <johnstiles@google.com>
Reviewed-by: Ethan Nicholas <ethannicholas@google.com>
diff --git a/gn/sksl_tests.gni b/gn/sksl_tests.gni
index 301161b..4306f3c 100644
--- a/gn/sksl_tests.gni
+++ b/gn/sksl_tests.gni
@@ -479,6 +479,7 @@
   "/sksl/inliner/InlinerWrapsEarlyReturnsWithForLoop.sksl",
   "/sksl/inliner/InlinerWrapsSwitchWithReturnInsideWithForLoop.sksl",
   "/sksl/inliner/ShortCircuitEvaluationsCannotInlineRightHandSide.sksl",
+  "/sksl/inliner/StaticSwitch.sksl",
   "/sksl/inliner/StructsCanBeInlinedSafely.sksl",
   "/sksl/inliner/SwitchWithCastCanBeInlined.sksl",
   "/sksl/inliner/SwitchWithoutReturnInsideCanBeInlined.sksl",
diff --git a/resources/sksl/inliner/StaticSwitch.sksl b/resources/sksl/inliner/StaticSwitch.sksl
new file mode 100644
index 0000000..1ef785a
--- /dev/null
+++ b/resources/sksl/inliner/StaticSwitch.sksl
@@ -0,0 +1,19 @@
+/*#pragma settings NoDeadCodeElimination*/
+
+uniform half4 colorGreen, colorRed;
+
+float get() {
+    @switch (2) {
+        case 1: return abs(1);
+        case 2: return abs(2);  // Only this case should be preserved.
+        case 3: return abs(3);
+        case 4: return abs(4);
+    }
+    // This won't be removed because dead-code elimination is disabled.
+    return abs(5);
+}
+
+half4 main() {
+    float result = get();
+    return result == 2 ? colorGreen : colorRed;
+}
diff --git a/src/sksl/SkSLCompiler.cpp b/src/sksl/SkSLCompiler.cpp
index fb3da30..1b2a469 100644
--- a/src/sksl/SkSLCompiler.cpp
+++ b/src/sksl/SkSLCompiler.cpp
@@ -732,7 +732,8 @@
         case Expression::Kind::kVariableReference: {
             const VariableReference& ref = expr->as<VariableReference>();
             const Variable* var = ref.variable();
-            if (ref.refKind() != VariableReference::RefKind::kWrite &&
+            if (fContext->fConfig->fSettings.fDeadCodeElimination &&
+                ref.refKind() != VariableReference::RefKind::kWrite &&
                 ref.refKind() != VariableReference::RefKind::kPointer &&
                 var->storage() == Variable::Storage::kLocal && !definitions.get(var) &&
                 optimizationContext->fSilences.find(var) == optimizationContext->fSilences.end()) {
@@ -1481,14 +1482,16 @@
     CFG cfg = CFGGenerator().getCFG(f);
     this->computeDataFlow(&cfg);
 
-    // check for unreachable code
-    for (size_t i = 0; i < cfg.fBlocks.size(); i++) {
-        const BasicBlock& block = cfg.fBlocks[i];
-        if (!block.fIsReachable && !block.fAllowUnreachable && block.fNodes.size()) {
-            const BasicBlock::Node& node = block.fNodes[0];
-            int offset = node.isStatement() ? (*node.statement())->fOffset
-                                            : (*node.expression())->fOffset;
-            this->error(offset, String("unreachable"));
+    if (fContext->fConfig->fSettings.fDeadCodeElimination) {
+        // Check for unreachable code.
+        for (size_t i = 0; i < cfg.fBlocks.size(); i++) {
+            const BasicBlock& block = cfg.fBlocks[i];
+            if (!block.fIsReachable && !block.fAllowUnreachable && block.fNodes.size()) {
+                const BasicBlock::Node& node = block.fNodes[0];
+                int offset = node.isStatement() ? (*node.statement())->fOffset
+                                                : (*node.expression())->fOffset;
+                this->error(offset, String("unreachable"));
+            }
         }
     }
     if (fErrorCount) {
@@ -1518,24 +1521,27 @@
             }
 
             BasicBlock& b = cfg.fBlocks[blockId];
-            if (blockId > 0 && !b.fIsReachable) {
-                // Block was reachable before optimization, but has since become unreachable. In
-                // addition to being dead code, it's broken - since control flow can't reach it, no
-                // prior variable definitions can reach it, and therefore variables might look to
-                // have not been properly assigned. Kill it by replacing all statements with Nops.
-                for (BasicBlock::Node& node : b.fNodes) {
-                    if (node.isStatement() && !(*node.statement())->is<Nop>()) {
-                        // Eliminating a node runs the risk of eliminating that node's exits as
-                        // well. Keep track of this and do a rescan if we are about to access one
-                        // of these.
-                        for (BlockId id : b.fExits) {
-                            eliminatedBlockIds.set(id);
+            if (fContext->fConfig->fSettings.fDeadCodeElimination) {
+                if (blockId > 0 && !b.fIsReachable) {
+                    // Block was reachable before optimization, but has since become unreachable. In
+                    // addition to being dead code, it's broken - since control flow can't reach it,
+                    // no prior variable definitions can reach it, and therefore variables might
+                    // look to have not been properly assigned. Kill it by replacing all statements
+                    // with Nops.
+                    for (BasicBlock::Node& node : b.fNodes) {
+                        if (node.isStatement() && !(*node.statement())->is<Nop>()) {
+                            // Eliminating a node runs the risk of eliminating that node's exits as
+                            // well. Keep track of this and do a rescan if we are about to access
+                            // one of these.
+                            for (BlockId id : b.fExits) {
+                                eliminatedBlockIds.set(id);
+                            }
+                            node.setStatement(std::make_unique<Nop>(), usage);
+                            madeChanges = true;
                         }
-                        node.setStatement(std::make_unique<Nop>(), usage);
-                        madeChanges = true;
                     }
+                    continue;
                 }
-                continue;
             }
             DefinitionMap definitions = b.fBefore;
 
diff --git a/src/sksl/SkSLMain.cpp b/src/sksl/SkSLMain.cpp
index 6af3ce1..b178145 100644
--- a/src/sksl/SkSLMain.cpp
+++ b/src/sksl/SkSLMain.cpp
@@ -204,6 +204,9 @@
                     static auto s_version450CoreCaps = Factory::Version450Core();
                     *caps = s_version450CoreCaps.get();
                 }
+                if (settingsText.consumeSuffix(" NoDeadCodeElimination")) {
+                    settings->fDeadCodeElimination = false;
+                }
                 if (settingsText.consumeSuffix(" FlipY")) {
                     settings->fFlipY = true;
                 }
diff --git a/src/sksl/SkSLProgramSettings.h b/src/sksl/SkSLProgramSettings.h
index bc13775..219624a 100644
--- a/src/sksl/SkSLProgramSettings.h
+++ b/src/sksl/SkSLProgramSettings.h
@@ -51,14 +51,17 @@
     // At present, zero is always used by our backends.
     int fDefaultUniformSet = 0;
     int fDefaultUniformBinding = 0;
-    // If true, remove any uncalled functions other than main(). Note that a function which
-    // starts out being used may end up being uncalled after optimization.
-    bool fRemoveDeadFunctions = true;
-    // Sets an upper limit on the acceptable amount of code growth from inlining.
-    // A value of zero will disable the inliner entirely.
-    int fInlineThreshold = SkSL::kDefaultInlineThreshold;
-    // true to enable optimization passes
+    // Enables the SkSL optimizer.
     bool fOptimize = true;
+    // (Requires fOptimize = true) Remove any uncalled functions other than main(). Note that a
+    // function which starts out being used may end up being uncalled after optimization.
+    bool fRemoveDeadFunctions = true;
+    // (Requires fOptimize = true) Uses the control-flow graph to detect and eliminate code within
+    // a function that has become unreachable due to optimization.
+    bool fDeadCodeElimination = true;
+    // (Requires fOptimize = true) When greater than zero, enables the inliner. The threshold value
+    // sets an upper limit on the acceptable amount of code growth from inlining.
+    int fInlineThreshold = SkSL::kDefaultInlineThreshold;
     // If true, implicit conversions to lower precision numeric types are allowed
     // (eg, float to half)
     bool fAllowNarrowingConversions = false;
diff --git a/src/sksl/SkSLSPIRVCodeGenerator.cpp b/src/sksl/SkSLSPIRVCodeGenerator.cpp
index 7610011..2b061eb 100644
--- a/src/sksl/SkSLSPIRVCodeGenerator.cpp
+++ b/src/sksl/SkSLSPIRVCodeGenerator.cpp
@@ -198,7 +198,7 @@
         case SpvOpSwitch:      // fall through
         case SpvOpBranch:      // fall through
         case SpvOpBranchConditional:
-            SkASSERT(fCurrentBlock);
+            SkASSERT(fCurrentBlock || !fContext.fConfig->fSettings.fDeadCodeElimination);
             fCurrentBlock = 0;
             break;
         case SpvOpConstant:          // fall through
@@ -236,7 +236,7 @@
         case SpvOpMemberDecorate:
             break;
         default:
-            SkASSERT(fCurrentBlock);
+            SkASSERT(fCurrentBlock || !fContext.fConfig->fSettings.fDeadCodeElimination);
     }
     this->writeWord((length << 16) | opCode, out);
 }
diff --git a/tests/sksl/inliner/StaticSwitch.glsl b/tests/sksl/inliner/StaticSwitch.glsl
new file mode 100644
index 0000000..2775fc0
--- /dev/null
+++ b/tests/sksl/inliner/StaticSwitch.glsl
@@ -0,0 +1,30 @@
+
+out vec4 sk_FragColor;
+uniform vec4 colorGreen;
+uniform vec4 colorRed;
+vec4 main() {
+    float _0_get;
+    for (int _1_loop = 0;_1_loop < 1; _1_loop++) {
+        {
+            {
+                _0_get = abs(2.0);
+                continue;
+            }
+            {
+                _0_get = abs(3.0);
+                continue;
+            }
+            {
+                _0_get = abs(4.0);
+                continue;
+            }
+        }
+        {
+            _0_get = abs(5.0);
+            continue;
+        }
+    }
+    float result = _0_get;
+
+    return result == 2.0 ? colorGreen : colorRed;
+}