Report an error if an out param is never written to.

GLSL ES2 behavior is explicitly undefined if an out-param is never
written to: "If a function does not write to an out parameter, the value
of the actual parameter is undefined when the function returns."

We do see divergence here in practice: SkVM's behavior (the parameter is
left alone) differs from my GPU's behavior (the parameter is zeroed
out).

SkSL will now report an error if an out parameter is never assigned-to.
There is no control flow analysis performed, so we will not report
cases where the out parameter is assigned-to on some paths but not
others. (Technically the return-on-all-paths logic could be adapted
for this, but it would be a fair amount of work.)

Structs are currently exempt from the rule because custom mesh
specifications require an `out` parameter for a Varyings struct, even if
your mesh program doesn't need Varyings.

Bug: skia:12867
Change-Id: Ie828d3ce91c2c67e008ae304fdb163ffa88d744c
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/500440
Auto-Submit: John Stiles <johnstiles@google.com>
Reviewed-by: Brian Osman <brianosman@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
diff --git a/gn/sksl_tests.gni b/gn/sksl_tests.gni
index 481a2cd..3c1bd88 100644
--- a/gn/sksl_tests.gni
+++ b/gn/sksl_tests.gni
@@ -215,6 +215,7 @@
   "/sksl/errors/SwizzleOutOfBounds.sksl",
   "/sksl/errors/SwizzleTooManyComponents.sksl",
   "/sksl/errors/TernaryMismatch.sksl",
+  "/sksl/errors/UnassignedOutParameter.sksl",
   "/sksl/errors/UndeclaredFunction.sksl",
   "/sksl/errors/UndefinedFunction.sksl",
   "/sksl/errors/UndefinedSymbol.sksl",
diff --git a/resources/sksl/errors/UnassignedOutParameter.sksl b/resources/sksl/errors/UnassignedOutParameter.sksl
new file mode 100644
index 0000000..990d00d
--- /dev/null
+++ b/resources/sksl/errors/UnassignedOutParameter.sksl
@@ -0,0 +1,11 @@
+// Expect 2 errors in "testOut"
+
+void testIn(in float2 a, in float2 b, in float2 c)             { a = float2(1); }
+void testOut(out float2 a, out float2 b, out float2 c)         { a = float2(1); }
+void testInout(inout float2 a, inout float2 b, inout float2 c) { a = float2(1); }
+
+void main(float2 p) {
+    testIn(p, p, p);
+    testOut(p, p, p);
+    testInout(p, p, p);
+}
diff --git a/resources/sksl/runtime/QualifierOrder.rts b/resources/sksl/runtime/QualifierOrder.rts
index 6d1ad4b..b08b282 100644
--- a/resources/sksl/runtime/QualifierOrder.rts
+++ b/resources/sksl/runtime/QualifierOrder.rts
@@ -7,7 +7,7 @@
 // These qualifiers are reversed from the expected order, but SkSL should compile and run anyway.
 noinline void const_after_in(in const vec2 x) {}
 noinline void inout_after_high_precision(highp inout vec2 x) {}
-noinline void out_after_high_precision(highp out vec2 x) {}
+noinline void out_after_high_precision(highp out vec2 x) { x = vec2(0); }
 
 vec4 main(vec2 coords) {
     const_after_in(coords);
diff --git a/src/sksl/SkSLAnalysis.h b/src/sksl/SkSLAnalysis.h
index 11143dc..4f6f616 100644
--- a/src/sksl/SkSLAnalysis.h
+++ b/src/sksl/SkSLAnalysis.h
@@ -175,6 +175,7 @@
  * Runs at finalization time to perform any last-minute correctness checks:
  * - Reports @if/@switch statements that didn't optimize away
  * - Reports dangling FunctionReference or TypeReference expressions
+ * - Reports function `out` params which are never written to (structs are currently exempt)
  */
 void DoFinalizationChecks(const Program& program);
 
diff --git a/src/sksl/analysis/SkSLFinalizationChecks.cpp b/src/sksl/analysis/SkSLFinalizationChecks.cpp
index 6a93ddf..f47afe2 100644
--- a/src/sksl/analysis/SkSLFinalizationChecks.cpp
+++ b/src/sksl/analysis/SkSLFinalizationChecks.cpp
@@ -13,35 +13,70 @@
 #include "src/sksl/analysis/SkSLProgramVisitor.h"
 #include "src/sksl/ir/SkSLFunctionCall.h"
 #include "src/sksl/ir/SkSLFunctionDeclaration.h"
+#include "src/sksl/ir/SkSLFunctionDefinition.h"
 #include "src/sksl/ir/SkSLIfStatement.h"
 #include "src/sksl/ir/SkSLProgram.h"
 #include "src/sksl/ir/SkSLSwitchStatement.h"
 #include "src/sksl/ir/SkSLVarDeclarations.h"
+#include "src/sksl/ir/SkSLVariable.h"
 
 namespace SkSL {
 namespace {
 
 class FinalizationVisitor : public ProgramVisitor {
 public:
-    FinalizationVisitor(const Context& ctx) : fContext(ctx) {}
+    FinalizationVisitor(const Context& c, const ProgramUsage& u) : fContext(c), fUsage(u) {}
 
     bool visitProgramElement(const ProgramElement& pe) override {
-        if (pe.kind() == ProgramElement::Kind::kGlobalVar) {
-            const VarDeclaration& decl =
-                    pe.as<GlobalVarDeclaration>().declaration()->as<VarDeclaration>();
-
-            size_t prevSlotsUsed = fGlobalSlotsUsed;
-            fGlobalSlotsUsed = SkSafeMath::Add(fGlobalSlotsUsed, decl.var().type().slotCount());
-            // To avoid overzealous error reporting, only trigger the error at the first
-            // place where the global limit is exceeded.
-            if (prevSlotsUsed < kVariableSlotLimit && fGlobalSlotsUsed >= kVariableSlotLimit) {
-                fContext.fErrors->error(pe.fLine, "global variable '" + decl.var().name() +
-                                                  "' exceeds the size limit");
+        switch (pe.kind()) {
+            case ProgramElement::Kind::kGlobalVar: {
+                this->checkGlobalVariableSizeLimit(pe.as<GlobalVarDeclaration>());
+                break;
             }
+            case ProgramElement::Kind::kFunction: {
+                this->checkOutParamsAreAssigned(pe.as<FunctionDefinition>());
+                break;
+            }
+            default:
+                break;
         }
         return INHERITED::visitProgramElement(pe);
     }
 
+    void checkGlobalVariableSizeLimit(const GlobalVarDeclaration& globalDecl) {
+        const VarDeclaration& decl = globalDecl.declaration()->as<VarDeclaration>();
+
+        size_t prevSlotsUsed = fGlobalSlotsUsed;
+        fGlobalSlotsUsed = SkSafeMath::Add(fGlobalSlotsUsed, decl.var().type().slotCount());
+        // To avoid overzealous error reporting, only trigger the error at the first place where the
+        // global limit is exceeded.
+        if (prevSlotsUsed < kVariableSlotLimit && fGlobalSlotsUsed >= kVariableSlotLimit) {
+            fContext.fErrors->error(decl.fLine, "global variable '" + decl.var().name() +
+                                                "' exceeds the size limit");
+        }
+    }
+
+    void checkOutParamsAreAssigned(const FunctionDefinition& funcDef) {
+        const FunctionDeclaration& funcDecl = funcDef.declaration();
+
+        // Searches for `out` parameters that are not written to. According to the GLSL spec,
+        // the value of an out-param that's never assigned to is unspecified, so report it.
+        // Structs are currently exempt from the rule because custom mesh specifications require an
+        // `out` parameter for a Varyings struct, even if your mesh program doesn't need Varyings.
+        for (const Variable* param : funcDecl.parameters()) {
+            const int paramInout = param->modifiers().fFlags & (Modifiers::Flag::kIn_Flag |
+                                                                Modifiers::Flag::kOut_Flag);
+            if (!param->type().isStruct() && paramInout == Modifiers::Flag::kOut_Flag) {
+                ProgramUsage::VariableCounts counts = fUsage.get(*param);
+                if (counts.fWrite <= 0) {
+                    fContext.fErrors->error(funcDecl.fLine,
+                                            "function '" + funcDecl.name() + "' never assigns a "
+                                            "value to out parameter '" + param->name() + "'");
+                }
+            }
+        }
+    }
+
     bool visitStatement(const Statement& stmt) override {
         if (!fContext.fConfig->fSettings.fPermitInvalidStaticTests) {
             switch (stmt.kind()) {
@@ -53,8 +88,7 @@
 
                 case Statement::Kind::kSwitch:
                     if (stmt.as<SwitchStatement>().isStatic()) {
-                        fContext.fErrors->error(stmt.fLine,
-                                                "static switch has non-static test");
+                        fContext.fErrors->error(stmt.fLine, "static switch has non-static test");
                     }
                     break;
 
@@ -95,13 +129,14 @@
     using INHERITED = ProgramVisitor;
     size_t fGlobalSlotsUsed = 0;
     const Context& fContext;
+    const ProgramUsage& fUsage;
 };
 
 }  // namespace
 
 void Analysis::DoFinalizationChecks(const Program& program) {
     // Check all of the program's owned elements. (Built-in elements are assumed to be valid.)
-    FinalizationVisitor visitor{*program.fContext};
+    FinalizationVisitor visitor{*program.fContext, *program.usage()};
     for (const std::unique_ptr<ProgramElement>& element : program.fOwnedElements) {
         visitor.visitProgramElement(*element);
     }
diff --git a/tests/sksl/errors/UnassignedOutParameter.glsl b/tests/sksl/errors/UnassignedOutParameter.glsl
new file mode 100644
index 0000000..453cd77
--- /dev/null
+++ b/tests/sksl/errors/UnassignedOutParameter.glsl
@@ -0,0 +1,5 @@
+### Compilation failed:
+
+error: 4: function 'testOut' never assigns a value to out parameter 'b'
+error: 4: function 'testOut' never assigns a value to out parameter 'c'
+2 errors
diff --git a/tests/sksl/runtime/QualifierOrder.stage b/tests/sksl/runtime/QualifierOrder.stage
index 3990477..be9e879 100644
--- a/tests/sksl/runtime/QualifierOrder.stage
+++ b/tests/sksl/runtime/QualifierOrder.stage
@@ -10,6 +10,7 @@
 }
 noinline void out_after_high_precision_0(out float2 x)
 {
+	x = float2(0.0);
 }
 float4 main(float2 coords)
 {