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:
diff --git a/src/sksl/SkSLDehydrator.cpp b/src/sksl/SkSLDehydrator.cpp
index f99f396..bce5b94 100644
--- a/src/sksl/SkSLDehydrator.cpp
+++ b/src/sksl/SkSLDehydrator.cpp
@@ -277,6 +277,11 @@
                 this->writeU8(b.value());
                 break;
             }
+
+            case Expression::Kind::kChildCall:
+                SkDEBUGFAIL("unimplemented--not expected to be used from within an include file");
+                break;
+
             case Expression::Kind::kCodeString:
                 SkDEBUGFAIL("shouldn't be able to receive kCodeString here");
                 break;
diff --git a/src/sksl/SkSLIRGenerator.cpp b/src/sksl/SkSLIRGenerator.cpp
index 40c1687..20d6307 100644
--- a/src/sksl/SkSLIRGenerator.cpp
+++ b/src/sksl/SkSLIRGenerator.cpp
@@ -25,6 +25,7 @@
 #include "src/sksl/ir/SkSLBinaryExpression.h"
 #include "src/sksl/ir/SkSLBoolLiteral.h"
 #include "src/sksl/ir/SkSLBreakStatement.h"
+#include "src/sksl/ir/SkSLChildCall.h"
 #include "src/sksl/ir/SkSLConstructor.h"
 #include "src/sksl/ir/SkSLContinueStatement.h"
 #include "src/sksl/ir/SkSLDiscardStatement.h"
@@ -1314,6 +1315,18 @@
 std::unique_ptr<Expression> IRGenerator::call(int offset,
                                               const FunctionDeclaration& function,
                                               ExpressionArray arguments) {
+    if (function.intrinsicKind() == k_sample_IntrinsicKind && arguments.size() >= 1 &&
+        arguments[0]->type().isEffectChild()) {
+        // Translate old-style sample(child, ...) calls into new-style child(...) IR
+        SkASSERT(arguments[0]->is<VariableReference>());
+        const Variable& child = *arguments[0]->as<VariableReference>().variable();
+        ExpressionArray argumentsWithoutChild;
+        for (size_t i = 1; i < arguments.size(); i++) {
+            argumentsWithoutChild.push_back(std::move(arguments[i]));
+        }
+        return ChildCall::Convert(fContext, offset, child, std::move(argumentsWithoutChild));
+    }
+
     if (function.isBuiltin()) {
         if (function.intrinsicKind() == k_dFdy_IntrinsicKind) {
             fInputs.fUseFlipRTUniform = true;
@@ -1413,6 +1426,16 @@
             }
             return this->call(offset, *functions[0], std::move(arguments));
         }
+        case Expression::Kind::kVariableReference: {
+            if (!functionValue->type().isEffectChild()) {
+                this->errorReporter().error(offset, "not a function");
+                return nullptr;
+            }
+            return ChildCall::Convert(fContext,
+                                      offset,
+                                      *functionValue->as<VariableReference>().variable(),
+                                      std::move(arguments));
+        }
         default:
             this->errorReporter().error(offset, "not a function");
             return nullptr;
diff --git a/src/sksl/SkSLInliner.cpp b/src/sksl/SkSLInliner.cpp
index 215f3aa..7f29001 100644
--- a/src/sksl/SkSLInliner.cpp
+++ b/src/sksl/SkSLInliner.cpp
@@ -16,6 +16,7 @@
 #include "src/sksl/ir/SkSLBinaryExpression.h"
 #include "src/sksl/ir/SkSLBoolLiteral.h"
 #include "src/sksl/ir/SkSLBreakStatement.h"
+#include "src/sksl/ir/SkSLChildCall.h"
 #include "src/sksl/ir/SkSLConstructor.h"
 #include "src/sksl/ir/SkSLConstructorArray.h"
 #include "src/sksl/ir/SkSLConstructorArrayCast.h"
@@ -311,6 +312,14 @@
         case Expression::Kind::kIntLiteral:
         case Expression::Kind::kFloatLiteral:
             return expression.clone();
+        case Expression::Kind::kChildCall: {
+            const ChildCall& childCall = expression.as<ChildCall>();
+            return ChildCall::Make(*fContext,
+                                   offset,
+                                   childCall.type().clone(symbolTableForExpression),
+                                   childCall.child(),
+                                   argList(childCall.arguments()));
+        }
         case Expression::Kind::kConstructorArray: {
             const ConstructorArray& ctor = expression.as<ConstructorArray>();
             return ConstructorArray::Make(*fContext, offset,
@@ -958,6 +967,13 @@
                 }
                 break;
             }
+            case Expression::Kind::kChildCall: {
+                ChildCall& childCallExpr = (*expr)->as<ChildCall>();
+                for (std::unique_ptr<Expression>& arg : childCallExpr.arguments()) {
+                    this->visitExpression(&arg);
+                }
+                break;
+            }
             case Expression::Kind::kConstructorArray:
             case Expression::Kind::kConstructorArrayCast:
             case Expression::Kind::kConstructorCompound:
diff --git a/src/sksl/codegen/SkSLPipelineStageCodeGenerator.cpp b/src/sksl/codegen/SkSLPipelineStageCodeGenerator.cpp
index 145a430..094f52f 100644
--- a/src/sksl/codegen/SkSLPipelineStageCodeGenerator.cpp
+++ b/src/sksl/codegen/SkSLPipelineStageCodeGenerator.cpp
@@ -13,6 +13,7 @@
 #include "src/sksl/SkSLOperators.h"
 #include "src/sksl/SkSLStringStream.h"
 #include "src/sksl/ir/SkSLBinaryExpression.h"
+#include "src/sksl/ir/SkSLChildCall.h"
 #include "src/sksl/ir/SkSLConstructor.h"
 #include "src/sksl/ir/SkSLConstructorArrayCast.h"
 #include "src/sksl/ir/SkSLDoStatement.h"
@@ -79,6 +80,7 @@
     void writeStructDefinition(const StructDefinition& s);
 
     void writeExpression(const Expression& expr, Precedence parentPrecedence);
+    void writeChildCall(const ChildCall& c);
     void writeFunctionCall(const FunctionCall& c);
     void writeAnyConstructor(const AnyConstructor& c, Precedence parentPrecedence);
     void writeFieldAccess(const FieldAccess& f);
@@ -138,76 +140,74 @@
     fBuffer->writeText("\n");
 }
 
-void PipelineStageCodeGenerator::writeFunctionCall(const FunctionCall& c) {
-    const FunctionDeclaration& function = c.function();
+void PipelineStageCodeGenerator::writeChildCall(const ChildCall& c) {
     const ExpressionArray& arguments = c.arguments();
-    if (function.isBuiltin() && function.name() == "sample") {
-        SkASSERT(arguments.size() >= 2);
-        const Expression* child = arguments[0].get();
-        SkASSERT(child->type().isEffectChild());
-        SkASSERT(child->is<VariableReference>());
-        int index = 0;
-        bool found = false;
-        for (const ProgramElement* p : fProgram.elements()) {
-            if (p->is<GlobalVarDeclaration>()) {
-                const GlobalVarDeclaration& global = p->as<GlobalVarDeclaration>();
-                const VarDeclaration& decl = global.declaration()->as<VarDeclaration>();
-                if (&decl.var() == child->as<VariableReference>().variable()) {
-                    found = true;
-                } else if (decl.var().type().isEffectChild()) {
-                    ++index;
-                }
+    SkASSERT(arguments.size() >= 1);
+    int index = 0;
+    bool found = false;
+    for (const ProgramElement* p : fProgram.elements()) {
+        if (p->is<GlobalVarDeclaration>()) {
+            const GlobalVarDeclaration& global = p->as<GlobalVarDeclaration>();
+            const VarDeclaration& decl = global.declaration()->as<VarDeclaration>();
+            if (&decl.var() == &c.child()) {
+                found = true;
+            } else if (decl.var().type().isEffectChild()) {
+                ++index;
             }
-            if (found) {
+        }
+        if (found) {
+            break;
+        }
+    }
+    SkASSERT(found);
+
+    // Shaders require a coordinate argument. Color filters require a color argument.
+    // Blenders require two color arguments.
+    String sampleOutput;
+    {
+        AutoOutputBuffer exprBuffer(this);
+        this->writeExpression(*arguments[0], Precedence::kSequence);
+
+        switch (c.child().type().typeKind()) {
+            case Type::TypeKind::kShader: {
+                SkASSERT(arguments.size() == 1);
+                SkASSERT(arguments[0]->type() == *fProgram.fContext->fTypes.fFloat2);
+                sampleOutput = fCallbacks->sampleShader(index, exprBuffer.fBuffer.str());
                 break;
             }
-        }
-        SkASSERT(found);
+            case Type::TypeKind::kColorFilter: {
+                SkASSERT(arguments.size() == 1);
+                SkASSERT(arguments[0]->type() == *fProgram.fContext->fTypes.fHalf4 ||
+                         arguments[0]->type() == *fProgram.fContext->fTypes.fFloat4);
+                sampleOutput = fCallbacks->sampleColorFilter(index, exprBuffer.fBuffer.str());
+                break;
+            }
+            case Type::TypeKind::kBlender: {
+                SkASSERT(arguments.size() == 2);
+                SkASSERT(arguments[0]->type() == *fProgram.fContext->fTypes.fHalf4 ||
+                         arguments[0]->type() == *fProgram.fContext->fTypes.fFloat4);
+                SkASSERT(arguments[1]->type() == *fProgram.fContext->fTypes.fHalf4 ||
+                         arguments[1]->type() == *fProgram.fContext->fTypes.fFloat4);
 
-        // Shaders require a coordinate argument. Color filters require a color argument.
-        // Blenders require two color arguments.
-        String sampleOutput;
-        {
-            AutoOutputBuffer exprBuffer(this);
-            this->writeExpression(*arguments[1], Precedence::kSequence);
+                AutoOutputBuffer exprBuffer2(this);
+                this->writeExpression(*arguments[1], Precedence::kSequence);
 
-            switch (child->type().typeKind()) {
-                case Type::TypeKind::kShader: {
-                    SkASSERT(arguments.size() == 2);
-                    SkASSERT(arguments[1]->type() == *fProgram.fContext->fTypes.fFloat2);
-                    sampleOutput = fCallbacks->sampleShader(index, exprBuffer.fBuffer.str());
-                    break;
-                }
-                case Type::TypeKind::kColorFilter: {
-                    SkASSERT(arguments.size() == 2);
-                    SkASSERT(arguments[1]->type() == *fProgram.fContext->fTypes.fHalf4 ||
-                             arguments[1]->type() == *fProgram.fContext->fTypes.fFloat4);
-                    sampleOutput = fCallbacks->sampleColorFilter(index, exprBuffer.fBuffer.str());
-                    break;
-                }
-                case Type::TypeKind::kBlender: {
-                    SkASSERT(arguments.size() == 3);
-                    SkASSERT(arguments[1]->type() == *fProgram.fContext->fTypes.fHalf4 ||
-                             arguments[1]->type() == *fProgram.fContext->fTypes.fFloat4);
-                    SkASSERT(arguments[2]->type() == *fProgram.fContext->fTypes.fHalf4 ||
-                             arguments[2]->type() == *fProgram.fContext->fTypes.fFloat4);
-
-                    AutoOutputBuffer exprBuffer2(this);
-                    this->writeExpression(*arguments[2], Precedence::kSequence);
-
-                    sampleOutput = fCallbacks->sampleBlender(index, exprBuffer.fBuffer.str(),
-                                                                    exprBuffer2.fBuffer.str());
-                    break;
-                }
-                default: {
-                    SkDEBUGFAILF("cannot sample from type '%s'",
-                                 child->type().description().c_str());
-                }
+                sampleOutput = fCallbacks->sampleBlender(index, exprBuffer.fBuffer.str(),
+                                                                exprBuffer2.fBuffer.str());
+                break;
+            }
+            default: {
+                SkDEBUGFAILF("cannot sample from type '%s'",
+                             c.child().type().description().c_str());
             }
         }
-        this->write(sampleOutput);
-        return;
     }
+    this->write(sampleOutput);
+    return;
+}
+
+void PipelineStageCodeGenerator::writeFunctionCall(const FunctionCall& c) {
+    const FunctionDeclaration& function = c.function();
 
     if (function.isBuiltin()) {
         this->write(function.name());
@@ -217,7 +217,7 @@
 
     this->write("(");
     const char* separator = "";
-    for (const auto& arg : arguments) {
+    for (const auto& arg : c.arguments()) {
         this->write(separator);
         separator = ", ";
         this->writeExpression(*arg, Precedence::kSequence);
@@ -435,6 +435,9 @@
         case Expression::Kind::kIntLiteral:
             this->write(expr.description());
             break;
+        case Expression::Kind::kChildCall:
+            this->writeChildCall(expr.as<ChildCall>());
+            break;
         case Expression::Kind::kConstructorArray:
         case Expression::Kind::kConstructorArrayCast:
         case Expression::Kind::kConstructorCompound:
diff --git a/src/sksl/codegen/SkSLVMCodeGenerator.cpp b/src/sksl/codegen/SkSLVMCodeGenerator.cpp
index b9e2333..d4369c1 100644
--- a/src/sksl/codegen/SkSLVMCodeGenerator.cpp
+++ b/src/sksl/codegen/SkSLVMCodeGenerator.cpp
@@ -17,6 +17,7 @@
 #include "src/sksl/ir/SkSLBlock.h"
 #include "src/sksl/ir/SkSLBoolLiteral.h"
 #include "src/sksl/ir/SkSLBreakStatement.h"
+#include "src/sksl/ir/SkSLChildCall.h"
 #include "src/sksl/ir/SkSLConstructor.h"
 #include "src/sksl/ir/SkSLConstructorArray.h"
 #include "src/sksl/ir/SkSLConstructorArrayCast.h"
@@ -181,6 +182,7 @@
     Value writeExpression(const Expression& expr);
     Value writeBinaryExpression(const BinaryExpression& b);
     Value writeAggregationConstructor(const AnyConstructor& c);
+    Value writeChildCall(const ChildCall& c);
     Value writeConstructorDiagonalMatrix(const ConstructorDiagonalMatrix& c);
     Value writeConstructorMatrixResize(const ConstructorMatrixResize& c);
     Value writeConstructorCast(const AnyConstructor& c);
@@ -865,76 +867,63 @@
     return result;
 }
 
+Value SkVMGenerator::writeChildCall(const ChildCall& c) {
+    auto child_it = fVariableMap.find(&c.child());
+    SkASSERT(child_it != fVariableMap.end());
+
+    const Expression* arg = c.arguments()[0].get();
+    Value argVal = this->writeExpression(*arg);
+    skvm::Color color;
+
+    switch (c.child().type().typeKind()) {
+        case Type::TypeKind::kShader: {
+            SkASSERT(c.arguments().size() == 1);
+            SkASSERT(arg->type() == *fProgram.fContext->fTypes.fFloat2);
+            skvm::Coord coord = {f32(argVal[0]), f32(argVal[1])};
+            color = fSampleShader(child_it->second, coord);
+            break;
+        }
+        case Type::TypeKind::kColorFilter: {
+            SkASSERT(c.arguments().size() == 1);
+            SkASSERT(arg->type() == *fProgram.fContext->fTypes.fHalf4 ||
+                     arg->type() == *fProgram.fContext->fTypes.fFloat4);
+            skvm::Color inColor = {f32(argVal[0]), f32(argVal[1]), f32(argVal[2]), f32(argVal[3])};
+            color = fSampleColorFilter(child_it->second, inColor);
+            break;
+        }
+        case Type::TypeKind::kBlender: {
+            SkASSERT(c.arguments().size() == 2);
+            SkASSERT(arg->type() == *fProgram.fContext->fTypes.fHalf4 ||
+                     arg->type() == *fProgram.fContext->fTypes.fFloat4);
+            skvm::Color srcColor = {f32(argVal[0]), f32(argVal[1]), f32(argVal[2]), f32(argVal[3])};
+
+            arg = c.arguments()[1].get();
+            argVal = this->writeExpression(*arg);
+            SkASSERT(arg->type() == *fProgram.fContext->fTypes.fHalf4 ||
+                     arg->type() == *fProgram.fContext->fTypes.fFloat4);
+            skvm::Color dstColor = {f32(argVal[0]), f32(argVal[1]), f32(argVal[2]), f32(argVal[3])};
+
+            color = fSampleBlender(child_it->second, srcColor, dstColor);
+            break;
+        }
+        default: {
+            SkDEBUGFAILF("cannot sample from type '%s'", c.child().type().description().c_str());
+        }
+    }
+
+    Value result(4);
+    result[0] = color.r;
+    result[1] = color.g;
+    result[2] = color.b;
+    result[3] = color.a;
+    return result;
+}
+
 Value SkVMGenerator::writeIntrinsicCall(const FunctionCall& c) {
     IntrinsicKind intrinsicKind = c.function().intrinsicKind();
     SkASSERT(intrinsicKind != kNotIntrinsic);
 
     const size_t nargs = c.arguments().size();
-
-    if (intrinsicKind == k_sample_IntrinsicKind) {
-        // Sample is very special. The first argument is a child (shader/colorFilter/blender),
-        // which is opaque and can't be evaluated.
-        SkASSERT(nargs >= 2);
-        const Expression* child = c.arguments()[0].get();
-        SkASSERT(child->type().isEffectChild());
-        SkASSERT(child->is<VariableReference>());
-
-        auto fp_it = fVariableMap.find(child->as<VariableReference>().variable());
-        SkASSERT(fp_it != fVariableMap.end());
-
-        // Shaders require a coordinate argument. Color filters require a color argument.
-        // When we call sampleChild, the other value remains the incoming default.
-        const Expression* arg = c.arguments()[1].get();
-        Value argVal = this->writeExpression(*arg);
-        skvm::Color color;
-
-        switch (child->type().typeKind()) {
-            case Type::TypeKind::kShader: {
-                SkASSERT(nargs == 2);
-                SkASSERT(arg->type() == *fProgram.fContext->fTypes.fFloat2);
-                skvm::Coord coord = {f32(argVal[0]), f32(argVal[1])};
-                color = fSampleShader(fp_it->second, coord);
-                break;
-            }
-            case Type::TypeKind::kColorFilter: {
-                SkASSERT(nargs == 2);
-                SkASSERT(arg->type() == *fProgram.fContext->fTypes.fHalf4 ||
-                         arg->type() == *fProgram.fContext->fTypes.fFloat4);
-                skvm::Color inColor = {f32(argVal[0]), f32(argVal[1]),
-                                       f32(argVal[2]), f32(argVal[3])};
-                color = fSampleColorFilter(fp_it->second, inColor);
-                break;
-            }
-            case Type::TypeKind::kBlender: {
-                SkASSERT(nargs == 3);
-                SkASSERT(arg->type() == *fProgram.fContext->fTypes.fHalf4 ||
-                         arg->type() == *fProgram.fContext->fTypes.fFloat4);
-                skvm::Color srcColor = {f32(argVal[0]), f32(argVal[1]),
-                                        f32(argVal[2]), f32(argVal[3])};
-
-                arg = c.arguments()[2].get();
-                argVal = this->writeExpression(*arg);
-                SkASSERT(arg->type() == *fProgram.fContext->fTypes.fHalf4 ||
-                         arg->type() == *fProgram.fContext->fTypes.fFloat4);
-                skvm::Color dstColor = {f32(argVal[0]), f32(argVal[1]),
-                                        f32(argVal[2]), f32(argVal[3])};
-
-                color = fSampleBlender(fp_it->second, srcColor, dstColor);
-                break;
-            }
-            default: {
-                SkDEBUGFAILF("cannot sample from type '%s'", child->type().description().c_str());
-            }
-        }
-
-        Value result(4);
-        result[0] = color.r;
-        result[1] = color.g;
-        result[2] = color.b;
-        result[3] = color.a;
-        return result;
-    }
-
     const size_t kMaxArgs = 3;  // eg: clamp, mix, smoothstep
     Value args[kMaxArgs];
     SkASSERT(nargs >= 1 && nargs <= SK_ARRAY_COUNT(args));
@@ -1336,6 +1325,8 @@
             return this->writeBinaryExpression(e.as<BinaryExpression>());
         case Expression::Kind::kBoolLiteral:
             return fBuilder->splat(e.as<BoolLiteral>().value() ? ~0 : 0);
+        case Expression::Kind::kChildCall:
+            return this->writeChildCall(e.as<ChildCall>());
         case Expression::Kind::kConstructorArray:
         case Expression::Kind::kConstructorCompound:
         case Expression::Kind::kConstructorStruct:
diff --git a/src/sksl/ir/SkSLChildCall.cpp b/src/sksl/ir/SkSLChildCall.cpp
new file mode 100644
index 0000000..bda1159
--- /dev/null
+++ b/src/sksl/ir/SkSLChildCall.cpp
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/sksl/SkSLContext.h"
+#include "src/sksl/ir/SkSLChildCall.h"
+
+namespace SkSL {
+
+bool ChildCall::hasProperty(Property property) const {
+    for (const auto& arg : this->arguments()) {
+        if (arg->hasProperty(property)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+std::unique_ptr<Expression> ChildCall::clone() const {
+    ExpressionArray cloned;
+    cloned.reserve_back(this->arguments().size());
+    for (const std::unique_ptr<Expression>& arg : this->arguments()) {
+        cloned.push_back(arg->clone());
+    }
+    return std::make_unique<ChildCall>(fOffset, &this->type(), &this->child(), std::move(cloned));
+}
+
+String ChildCall::description() const {
+    String result = String(this->child().name()) + "(";
+    String separator;
+    for (const std::unique_ptr<Expression>& arg : this->arguments()) {
+        result += separator;
+        result += arg->description();
+        separator = ", ";
+    }
+    result += ")";
+    return result;
+}
+
+struct ChildCallSignature {
+    const Type*               fReturnType = nullptr;
+    SkSTArray<2, const Type*> fParamTypes;
+};
+
+static ChildCallSignature child_call_signature(const Context& context, const Variable& child) {
+    const Type* half4  = context.fTypes.fHalf4.get();
+    const Type* float2 = context.fTypes.fFloat2.get();
+
+    switch (child.type().typeKind()) {
+        case Type::TypeKind::kBlender:     return { half4, { half4, half4 } };
+        case Type::TypeKind::kColorFilter: return { half4, { half4 } };
+        case Type::TypeKind::kShader:      return { half4, { float2 } };
+        default:
+            SkUNREACHABLE;
+    }
+}
+
+std::unique_ptr<Expression> ChildCall::Convert(const Context& context,
+                                               int offset,
+                                               const Variable& child,
+                                               ExpressionArray arguments) {
+    ChildCallSignature signature = child_call_signature(context, child);
+    skstd::string_view typeName = child.type().name();
+
+    // Reject function calls with the wrong number of arguments.
+    if (signature.fParamTypes.size() != arguments.size()) {
+        String msg = "call to '" + typeName + "' expected " +
+                     to_string((int)signature.fParamTypes.size()) + " argument";
+        if (signature.fParamTypes.size() != 1) {
+            msg += "s";
+        }
+        msg += ", but found " + to_string(arguments.count());
+        context.errors().error(offset, msg);
+        return nullptr;
+    }
+
+    for (size_t i = 0; i < arguments.size(); i++) {
+        // Coerce each argument to the proper type.
+        arguments[i] = signature.fParamTypes[i]->coerceExpression(std::move(arguments[i]), context);
+        if (!arguments[i]) {
+            return nullptr;
+        }
+    }
+
+    return Make(context, offset, signature.fReturnType, child, std::move(arguments));
+}
+
+std::unique_ptr<Expression> ChildCall::Make(const Context& context,
+                                            int offset,
+                                            const Type* returnType,
+                                            const Variable& child,
+                                            ExpressionArray arguments) {
+#ifdef SK_DEBUG
+    ChildCallSignature signature = child_call_signature(context, child);
+    SkASSERT(signature.fParamTypes.size() == arguments.size());
+    for (size_t i = 0; i < arguments.size(); i++) {
+        SkASSERT(arguments[i]->type() == *signature.fParamTypes[i]);
+    }
+#endif
+
+    return std::make_unique<ChildCall>(offset, returnType, &child, std::move(arguments));
+}
+
+}  // namespace SkSL
diff --git a/src/sksl/ir/SkSLChildCall.h b/src/sksl/ir/SkSLChildCall.h
new file mode 100644
index 0000000..cf3c477
--- /dev/null
+++ b/src/sksl/ir/SkSLChildCall.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SKSL_CHILDCALL
+#define SKSL_CHILDCALL
+
+#include "include/private/SkTArray.h"
+#include "src/sksl/ir/SkSLExpression.h"
+#include "src/sksl/ir/SkSLVariable.h"
+
+namespace SkSL {
+
+/**
+ * A call to a child effect object (shader, color filter, or blender).
+ */
+class ChildCall final : public Expression {
+public:
+    static constexpr Kind kExpressionKind = Kind::kChildCall;
+
+    ChildCall(int offset, const Type* type, const Variable* child, ExpressionArray arguments)
+            : INHERITED(offset, kExpressionKind, type)
+            , fChild(*child)
+            , fArguments(std::move(arguments)) {}
+
+    // Performs type conversion on arguments, determines return type, and reports errors via the
+    // ErrorReporter.
+    static std::unique_ptr<Expression> Convert(const Context& context,
+                                               int offset,
+                                               const Variable& child,
+                                               ExpressionArray arguments);
+
+    // Creates the child call; reports errors via ASSERT.
+    static std::unique_ptr<Expression> Make(const Context& context,
+                                            int offset,
+                                            const Type* returnType,
+                                            const Variable& child,
+                                            ExpressionArray arguments);
+
+    const Variable& child() const {
+        return fChild;
+    }
+
+    ExpressionArray& arguments() {
+        return fArguments;
+    }
+
+    const ExpressionArray& arguments() const {
+        return fArguments;
+    }
+
+    bool hasProperty(Property property) const override;
+
+    std::unique_ptr<Expression> clone() const override;
+
+    String description() const override;
+
+private:
+    const Variable& fChild;
+    ExpressionArray fArguments;
+
+    using INHERITED = Expression;
+};
+
+}  // namespace SkSL
+
+#endif
diff --git a/src/sksl/ir/SkSLExpression.h b/src/sksl/ir/SkSLExpression.h
index 149d5e6..76073bb 100644
--- a/src/sksl/ir/SkSLExpression.h
+++ b/src/sksl/ir/SkSLExpression.h
@@ -29,6 +29,7 @@
     enum class Kind {
         kBinary = (int) Statement::Kind::kLast + 1,
         kBoolLiteral,
+        kChildCall,
         kCodeString,
         kConstructorArray,
         kConstructorArrayCast,