SkSL: Allow invoking children (shaders, etc) like functions

Previously, you would declare child objects (shaders, colorFilters, etc.)
and "sample" them like this:

    uniform shader input;
    uniform colorFilter filter;
    half4 main(float2 coord) {
        half4 inColor = sample(input, coord);
        return sample(filter, inColor);
    }

With the new syntax, those child objects become directly callable,
reflecting the way that Skia assembles all parts of the paint (as functions)
in the overall fragment shader:

    uniform shader input;
    uniform colorFilter filter;
    half4 main(float2 coord) {
        half4 inColor = input(coord);
        return filter(inColor);
    }

Bug: skia:12302
Change-Id: Ia12351964dc5d2300660187933188e738671cd83
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/436517
Commit-Queue: Brian Osman <brianosman@google.com>
Reviewed-by: Brian Salomon <bsalomon@google.com>
Reviewed-by: John Stiles <johnstiles@google.com>
diff --git a/src/sksl/SkSLAnalysis.cpp b/src/sksl/SkSLAnalysis.cpp
index 6b9dcc7..bd2782e 100644
--- a/src/sksl/SkSLAnalysis.cpp
+++ b/src/sksl/SkSLAnalysis.cpp
@@ -38,6 +38,7 @@
 // Expressions
 #include "src/sksl/ir/SkSLBinaryExpression.h"
 #include "src/sksl/ir/SkSLBoolLiteral.h"
+#include "src/sksl/ir/SkSLChildCall.h"
 #include "src/sksl/ir/SkSLConstructor.h"
 #include "src/sksl/ir/SkSLConstructorDiagonalMatrix.h"
 #include "src/sksl/ir/SkSLConstructorMatrixResize.h"
@@ -62,18 +63,13 @@
 
 namespace {
 
-static bool is_sample_call_to_fp(const FunctionCall& fc, const Variable& fp) {
-    const FunctionDeclaration& f = fc.function();
-    return f.intrinsicKind() == k_sample_IntrinsicKind && fc.arguments().size() >= 1 &&
-           fc.arguments()[0]->is<VariableReference>() &&
-           fc.arguments()[0]->as<VariableReference>().variable() == &fp;
-}
-
-// Visitor that determines the merged SampleUsage for a given child 'fp' in the program.
+// Visitor that determines the merged SampleUsage for a given child in the program.
 class MergeSampleUsageVisitor : public ProgramVisitor {
 public:
-    MergeSampleUsageVisitor(const Context& context, const Variable& fp, bool writesToSampleCoords)
-            : fContext(context), fFP(fp), fWritesToSampleCoords(writesToSampleCoords) {}
+    MergeSampleUsageVisitor(const Context& context,
+                            const Variable& child,
+                            bool writesToSampleCoords)
+            : fContext(context), fChild(child), fWritesToSampleCoords(writesToSampleCoords) {}
 
     SampleUsage visit(const Program& program) {
         fUsage = SampleUsage(); // reset to none
@@ -85,43 +81,34 @@
 
 protected:
     const Context& fContext;
-    const Variable& fFP;
+    const Variable& fChild;
     const bool fWritesToSampleCoords;
     SampleUsage fUsage;
     int fElidedSampleCoordCount = 0;
 
     bool visitExpression(const Expression& e) override {
-        // Looking for sample(fp, ...)
-        if (e.is<FunctionCall>()) {
-            const FunctionCall& fc = e.as<FunctionCall>();
-            if (is_sample_call_to_fp(fc, fFP)) {
-                // Determine the type of call at this site, and merge it with the accumulated state
-                if (fc.arguments().size() >= 2) {
-                    const Expression* coords = fc.arguments()[1].get();
-                    if (coords->type() == *fContext.fTypes.fFloat2) {
-                        // If the coords are a direct reference to the program's sample-coords,
-                        // and those coords are never modified, we can conservatively turn this
-                        // into PassThrough sampling. In all other cases, we consider it Explicit.
-                        if (!fWritesToSampleCoords && coords->is<VariableReference>() &&
-                            coords->as<VariableReference>()
-                                            .variable()
-                                            ->modifiers()
-                                            .fLayout.fBuiltin == SK_MAIN_COORDS_BUILTIN) {
-                            fUsage.merge(SampleUsage::PassThrough());
-                            ++fElidedSampleCoordCount;
-                        } else {
-                            fUsage.merge(SampleUsage::Explicit());
-                        }
-                    } else {
-                        // sample(fp, half4 inputColor) -> PassThrough
-                        fUsage.merge(SampleUsage::PassThrough());
-                    }
-                } else {
-                    // sample(fp) -> PassThrough
+        // Looking for child(...)
+        if (e.is<ChildCall>() && &e.as<ChildCall>().child() == &fChild) {
+            // Determine the type of call at this site, and merge it with the accumulated state
+            const ExpressionArray& arguments = e.as<ChildCall>().arguments();
+            SkASSERT(arguments.size() >= 1);
+
+            const Expression* maybeCoords = arguments[0].get();
+            if (maybeCoords->type() == *fContext.fTypes.fFloat2) {
+                // If the coords are a direct reference to the program's sample-coords, and those
+                // coords are never modified, we can conservatively turn this into PassThrough
+                // sampling. In all other cases, we consider it Explicit.
+                if (!fWritesToSampleCoords && maybeCoords->is<VariableReference>() &&
+                    maybeCoords->as<VariableReference>().variable()->modifiers().fLayout.fBuiltin ==
+                            SK_MAIN_COORDS_BUILTIN) {
                     fUsage.merge(SampleUsage::PassThrough());
+                    ++fElidedSampleCoordCount;
+                } else {
+                    fUsage.merge(SampleUsage::Explicit());
                 }
-                // NOTE: we don't return true here just because we found a sample call. We need to
-                // process the entire program and merge across all encountered calls.
+            } else {
+                // child(inputColor) or child(srcColor, dstColor) -> PassThrough
+                fUsage.merge(SampleUsage::PassThrough());
             }
         }
 
@@ -149,17 +136,14 @@
     using INHERITED = ProgramVisitor;
 };
 
-// Visitor that searches for calls to sample() from a function other than main()
+// Visitor that searches for child calls from a function other than main()
 class SampleOutsideMainVisitor : public ProgramVisitor {
 public:
     SampleOutsideMainVisitor() {}
 
     bool visitExpression(const Expression& e) override {
-        if (e.is<FunctionCall>()) {
-            const FunctionDeclaration& f = e.as<FunctionCall>().function();
-            if (f.intrinsicKind() == k_sample_IntrinsicKind) {
-                return true;
-            }
+        if (e.is<ChildCall>()) {
+            return true;
         }
         return INHERITED::visitExpression(e);
     }
@@ -1199,6 +1183,7 @@
             // These are completely disallowed in SkSL constant-(index)-expressions. GLSL allows
             // calls to built-in functions where the arguments are all constant-expressions, but
             // we don't guarantee that behavior. (skbug.com/10835)
+            case Expression::Kind::kChildCall:
             case Expression::Kind::kExternalFunctionCall:
             case Expression::Kind::kFunctionCall:
                 return true;
@@ -1311,6 +1296,14 @@
             return (b.left() && this->visitExpressionPtr(b.left())) ||
                    (b.right() && this->visitExpressionPtr(b.right()));
         }
+        case Expression::Kind::kChildCall: {
+            // We don't visit the child variable itself, just the arguments
+            auto& c = e.template as<ChildCall>();
+            for (auto& arg : c.arguments()) {
+                if (arg && this->visitExpressionPtr(arg)) { return true; }
+            }
+            return false;
+        }
         case Expression::Kind::kConstructorArray:
         case Expression::Kind::kConstructorArrayCast:
         case Expression::Kind::kConstructorCompound: