Răsfoiți Sursa

Move AExpression mutable members into isolated Input/Output objects (#53075)

This is a step toward the long-term goal of making the "user" trees nodes 
immutable. This change isolates the mutable data for expression nodes in 
the "user" tree during the semantic (analysis) phase by moving the 
mutable data into Input and Output objects. These objects are created 
locally during the semantic phase to share information required for 
semantic checking between parent-child nodes.

Note that for this change, Input and Output are still stored in a mutable 
way on the expression nodes. This will not be the case once the semantic 
(analysis) phase and ir generation (write) phase are combined in a future 
change.

Relates #49869
Jack Conradson 5 ani în urmă
părinte
comite
c70c48d69c
57 a modificat fișierele cu 946 adăugiri și 1229 ștergeri
  1. 30 28
      modules/lang-painless/src/main/java/org/elasticsearch/painless/Operation.java
  2. 56 45
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/AExpression.java
  3. 14 5
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/AStoreable.java
  4. 96 102
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EAssignment.java
  5. 61 389
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBinary.java
  6. 14 7
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBool.java
  7. 9 4
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBoolean.java
  8. 16 10
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java
  9. 11 6
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECapturingFunctionRef.java
  10. 25 218
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EComp.java
  11. 31 21
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EConditional.java
  12. 20 12
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EConstant.java
  13. 10 5
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EDecimal.java
  14. 25 18
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EElvis.java
  15. 12 6
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EExplicit.java
  16. 12 6
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EFunctionRef.java
  17. 15 10
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EInstanceof.java
  18. 15 10
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ELambda.java
  19. 17 11
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EListInit.java
  20. 21 14
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EMapInit.java
  21. 13 7
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewArray.java
  22. 11 6
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewArrayFunctionRef.java
  23. 18 12
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewObj.java
  24. 13 8
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENull.java
  25. 16 11
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENumeric.java
  26. 9 3
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ERegex.java
  27. 9 4
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EStatic.java
  28. 9 4
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EString.java
  29. 33 78
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EUnary.java
  30. 20 4
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EVariable.java
  31. 36 19
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PBrace.java
  32. 20 14
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PCallInvoke.java
  33. 43 25
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PField.java
  34. 9 4
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubArrayLength.java
  35. 11 5
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubBrace.java
  36. 13 7
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubCallInvoke.java
  37. 11 6
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefArray.java
  38. 14 8
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefCall.java
  39. 9 4
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefField.java
  40. 9 4
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubField.java
  41. 12 6
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubListShortcut.java
  42. 12 6
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubMapShortcut.java
  43. 10 5
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubNullSafeCallInvoke.java
  44. 13 7
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubNullSafeField.java
  45. 9 4
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubShortcut.java
  46. 4 2
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclaration.java
  47. 3 2
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDo.java
  48. 5 5
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SEach.java
  49. 8 6
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SExpression.java
  50. 13 10
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SFor.java
  51. 3 2
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SIf.java
  52. 3 2
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SIfElse.java
  53. 4 3
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SReturn.java
  54. 2 2
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSubEachArray.java
  55. 3 3
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSubEachIterable.java
  56. 3 2
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SThrow.java
  57. 3 2
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SWhile.java

+ 30 - 28
modules/lang-painless/src/main/java/org/elasticsearch/painless/Operation.java

@@ -27,37 +27,39 @@ package org.elasticsearch.painless;
  */
 public enum Operation {
 
-    MUL     ( "*"   ),
-    DIV     ( "/"   ),
-    REM     ( "%"   ),
-    ADD     ( "+"   ),
-    SUB     ( "-"   ),
-    FIND    ( "=~"  ),
-    MATCH   ( "==~" ),
-    LSH     ( "<<"  ),
-    RSH     ( ">>"  ),
-    USH     ( ">>>" ),
-    BWNOT   ( "~"   ),
-    BWAND   ( "&"   ),
-    XOR     ( "^"   ),
-    BWOR    ( "|"   ),
-    NOT     ( "!"   ),
-    AND     ( "&&"  ),
-    OR      ( "||"  ),
-    LT      ( "<"   ),
-    LTE     ( "<="  ),
-    GT      ( ">"   ),
-    GTE     ( ">="  ),
-    EQ      ( "=="  ),
-    EQR     ( "===" ),
-    NE      ( "!="  ),
-    NER     ( "!==" ),
-    INCR    ( "++"  ),
-    DECR    ( "--"  );
+    MUL     ( "*"   , "multiplication"         ),
+    DIV     ( "/"   , "division"               ),
+    REM     ( "%"   , "remainder"              ),
+    ADD     ( "+"   , "addition"               ),
+    SUB     ( "-"   , "subtraction"            ),
+    FIND    ( "=~"  , "find"                   ),
+    MATCH   ( "==~" , "match"                  ),
+    LSH     ( "<<"  , "left shift"             ),
+    RSH     ( ">>"  , "right shift"            ),
+    USH     ( ">>>" , "unsigned shift"         ),
+    BWNOT   ( "~"   , "bitwise not"            ),
+    BWAND   ( "&"   , "bitwise and"            ),
+    XOR     ( "^"   , "bitwise xor"            ),
+    BWOR    ( "|"   , "boolean or"             ),
+    NOT     ( "!"   , "boolean not"            ),
+    AND     ( "&&"  , "boolean and"            ),
+    OR      ( "||"  , "boolean or"             ),
+    LT      ( "<"   , "less than"              ),
+    LTE     ( "<="  , "less than or equals"    ),
+    GT      ( ">"   , "greater than"           ),
+    GTE     ( ">="  , "greater than or equals" ),
+    EQ      ( "=="  , "equals"                 ),
+    EQR     ( "===" , "reference equals"       ),
+    NE      ( "!="  , "not equals"             ),
+    NER     ( "!==" , "reference not equals"   ),
+    INCR    ( "++"  , "increment"              ),
+    DECR    ( "--"  , "decrement"              );
 
     public final String symbol;
+    public final String name;
 
-    Operation(final String symbol) {
+    Operation(final String symbol, final String name) {
         this.symbol = symbol;
+        this.name = name;
     }
 }

+ 56 - 45
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/AExpression.java

@@ -35,6 +35,52 @@ import java.util.Objects;
  */
 public abstract class AExpression extends ANode {
 
+    public static class Input {
+
+        /**
+         * Set to false when an expression will not be read from such as
+         * a basic assignment.  Note this variable is always set by the parent
+         * as input.
+         */
+        boolean read = true;
+
+        /**
+         * Set to the expected type this node needs to be.  Note this variable
+         * is always set by the parent as input and should never be read from.
+         */
+        Class<?> expected = null;
+
+        /**
+         * Set by {@link EExplicit} if a cast made on an expression node should be
+         * explicit.
+         */
+        boolean explicit = false;
+
+        /**
+         * Set to true if a cast is allowed to boxed/unboxed.  This is used
+         * for method arguments because casting may be required.
+         */
+        boolean internal = false;
+    }
+
+    public static class Output {
+
+        /**
+         * Set to true when an expression can be considered a stand alone
+         * statement.  Used to prevent extraneous bytecode. This is always
+         * set by the node as output.
+         */
+        boolean statement = false;
+
+        /**
+         * Set to the actual type this node is.  Note this variable is always
+         * set by the node as output and should only be read from outside of the
+         * node itself.  <b>Also, actual can always be read after a cast is
+         * called on this node to get the type of the node after the cast.</b>
+         */
+        Class<?> actual = null;
+    }
+
     /**
      * Prefix is the predecessor to this node in a variable chain.
      * This is used to analyze and write variable chains in a
@@ -44,50 +90,13 @@ public abstract class AExpression extends ANode {
      */
     AExpression prefix;
 
-    /**
-     * Set to false when an expression will not be read from such as
-     * a basic assignment.  Note this variable is always set by the parent
-     * as input.
-     */
-    boolean read = true;
-
-    /**
-     * Set to true when an expression can be considered a stand alone
-     * statement.  Used to prevent extraneous bytecode. This is always
-     * set by the node as output.
-     */
-    boolean statement = false;
-
-    /**
-     * Set to the expected type this node needs to be.  Note this variable
-     * is always set by the parent as input and should never be read from.
-     */
-    Class<?> expected = null;
-
-    /**
-     * Set to the actual type this node is.  Note this variable is always
-     * set by the node as output and should only be read from outside of the
-     * node itself.  <b>Also, actual can always be read after a cast is
-     * called on this node to get the type of the node after the cast.</b>
-     */
-    Class<?> actual = null;
-
-    /**
-     * Set by {@link EExplicit} if a cast made on an expression node should be
-     * explicit.
-     */
-    boolean explicit = false;
-
-    /**
-     * Set to true if a cast is allowed to boxed/unboxed.  This is used
-     * for method arguments because casting may be required.
-     */
-    boolean internal = false;
-
-    // This is used to support the transition from a mutable to immutable state.
+    // TODO: remove placeholders once analysis and write are combined into build
+    // This are used to support the transition from a mutable to immutable state.
     // Currently, the IR tree is built during the user tree "write" phase, so
-    // this is stored on the node to set during the "semantic" phase and then
+    // these are stored on the node to set during the "semantic" phase and then
     // use during the "write" phase.
+    Input input = null;
+    Output output = null;
     PainlessCast cast = null;
 
     /**
@@ -111,7 +120,9 @@ public abstract class AExpression extends ANode {
     /**
      * Checks for errors and collects data for the writing phase.
      */
-    abstract void analyze(ScriptRoot scriptRoot, Scope scope);
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        throw new UnsupportedOperationException();
+    }
 
     /**
      * Writes ASM based on the data collected during the analysis phase.
@@ -119,7 +130,7 @@ public abstract class AExpression extends ANode {
     abstract ExpressionNode write(ClassNode classNode);
 
     void cast() {
-        cast = AnalyzerCaster.getLegalCast(location, actual, expected, explicit, internal);
+        cast = AnalyzerCaster.getLegalCast(location, output.actual, input.expected, input.explicit, input.internal);
     }
 
     ExpressionNode cast(ExpressionNode expressionNode) {
@@ -129,7 +140,7 @@ public abstract class AExpression extends ANode {
 
         CastNode castNode = new CastNode();
         castNode.setLocation(location);
-        castNode.setExpressionType(expected);
+        castNode.setExpressionType(cast.targetType);
         castNode.setCast(cast);
         castNode.setChildNode(expressionNode);
 

+ 14 - 5
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/AStoreable.java

@@ -20,6 +20,8 @@
 package org.elasticsearch.painless.node;
 
 import org.elasticsearch.painless.Location;
+import org.elasticsearch.painless.Scope;
+import org.elasticsearch.painless.symbol.ScriptRoot;
 
 import java.util.Objects;
 
@@ -28,11 +30,14 @@ import java.util.Objects;
  */
 abstract class AStoreable extends AExpression {
 
-    /**
-     * Set to true when this node is an lhs-expression and will be storing
-     * a value from an rhs-expression.
-     */
-    boolean write = false;
+    public static class Input extends AExpression.Input {
+
+        /**
+         * Set to true when this node is an lhs-expression and will be storing
+         * a value from an rhs-expression.
+         */
+        boolean write = false;
+    }
 
     /**
      * Standard constructor with location used for error tracking.
@@ -52,6 +57,10 @@ abstract class AStoreable extends AExpression {
         this.prefix = Objects.requireNonNull(prefix);
     }
 
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        throw new UnsupportedOperationException();
+    }
+
     /**
      * Returns true if this node or a sub-node of this node can be optimized with
      * rhs actual type to avoid an unnecessary cast.

+ 96 - 102
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EAssignment.java

@@ -62,32 +62,24 @@ public final class EAssignment extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        analyzeLHS(scriptRoot, scope);
-        analyzeIncrDecr();
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
 
-        if (operation != null) {
-            analyzeCompound(scriptRoot, scope);
-        } else if (rhs != null) {
-            analyzeSimple(scriptRoot, scope);
-        } else {
-            throw new IllegalStateException("Illegal tree structure.");
-        }
-    }
+        Output leftOutput;
+        Output rightOutput;
 
-    private void analyzeLHS(ScriptRoot scriptRoot, Scope scope) {
         if (lhs instanceof AStoreable) {
             AStoreable lhs = (AStoreable)this.lhs;
+            AStoreable.Input leftInput = new AStoreable.Input();
 
-            lhs.read = read;
-            lhs.write = true;
-            lhs.analyze(scriptRoot, scope);
+            leftInput.read = input.read;
+            leftInput.write = true;
+            leftOutput = lhs.analyze(scriptRoot, scope, leftInput);
         } else {
             throw new IllegalArgumentException("Left-hand side cannot be assigned a value.");
         }
-    }
 
-    private void analyzeIncrDecr() {
         if (pre && post) {
             throw createError(new IllegalStateException("Illegal tree structure."));
         } else if (pre || post) {
@@ -96,11 +88,11 @@ public final class EAssignment extends AExpression {
             }
 
             if (operation == Operation.INCR) {
-                if (lhs.actual == double.class) {
+                if (leftOutput.actual == double.class) {
                     rhs = new EConstant(location, 1D);
-                } else if (lhs.actual == float.class) {
+                } else if (leftOutput.actual == float.class) {
                     rhs = new EConstant(location, 1F);
-                } else if (lhs.actual == long.class) {
+                } else if (leftOutput.actual == long.class) {
                     rhs = new EConstant(location, 1L);
                 } else {
                     rhs = new EConstant(location, 1);
@@ -108,11 +100,11 @@ public final class EAssignment extends AExpression {
 
                 operation = Operation.ADD;
             } else if (operation == Operation.DECR) {
-                if (lhs.actual == double.class) {
+                if (leftOutput.actual == double.class) {
                     rhs = new EConstant(location, 1D);
-                } else if (lhs.actual == float.class) {
+                } else if (leftOutput.actual == float.class) {
                     rhs = new EConstant(location, 1F);
-                } else if (lhs.actual == long.class) {
+                } else if (leftOutput.actual == long.class) {
                     rhs = new EConstant(location, 1L);
                 } else {
                     rhs = new EConstant(location, 1);
@@ -123,103 +115,105 @@ public final class EAssignment extends AExpression {
                 throw createError(new IllegalStateException("Illegal tree structure."));
             }
         }
-    }
 
-    private void analyzeCompound(ScriptRoot scriptRoot, Scope scope) {
-        rhs.analyze(scriptRoot, scope);
-        boolean shift = false;
-
-        if (operation == Operation.MUL) {
-            promote = AnalyzerCaster.promoteNumeric(lhs.actual, rhs.actual, true);
-        } else if (operation == Operation.DIV) {
-            promote = AnalyzerCaster.promoteNumeric(lhs.actual, rhs.actual, true);
-        } else if (operation == Operation.REM) {
-            promote = AnalyzerCaster.promoteNumeric(lhs.actual, rhs.actual, true);
-        } else if (operation == Operation.ADD) {
-            promote = AnalyzerCaster.promoteAdd(lhs.actual, rhs.actual);
-        } else if (operation == Operation.SUB) {
-            promote = AnalyzerCaster.promoteNumeric(lhs.actual, rhs.actual, true);
-        } else if (operation == Operation.LSH) {
-            promote = AnalyzerCaster.promoteNumeric(lhs.actual, false);
-            shiftDistance = AnalyzerCaster.promoteNumeric(rhs.actual, false);
-            shift = true;
-        } else if (operation == Operation.RSH) {
-            promote = AnalyzerCaster.promoteNumeric(lhs.actual, false);
-            shiftDistance = AnalyzerCaster.promoteNumeric(rhs.actual, false);
-            shift = true;
-        } else if (operation == Operation.USH) {
-            promote = AnalyzerCaster.promoteNumeric(lhs.actual, false);
-            shiftDistance = AnalyzerCaster.promoteNumeric(rhs.actual, false);
-            shift = true;
-        } else if (operation == Operation.BWAND) {
-            promote = AnalyzerCaster.promoteXor(lhs.actual, rhs.actual);
-        } else if (operation == Operation.XOR) {
-            promote = AnalyzerCaster.promoteXor(lhs.actual, rhs.actual);
-        } else if (operation == Operation.BWOR) {
-            promote = AnalyzerCaster.promoteXor(lhs.actual, rhs.actual);
-        } else {
-            throw createError(new IllegalStateException("Illegal tree structure."));
-        }
+        if (operation != null) {
+            rightOutput = rhs.analyze(scriptRoot, scope, new Input());
+            boolean shift = false;
+
+            if (operation == Operation.MUL) {
+                promote = AnalyzerCaster.promoteNumeric(leftOutput.actual, rightOutput.actual, true);
+            } else if (operation == Operation.DIV) {
+                promote = AnalyzerCaster.promoteNumeric(leftOutput.actual, rightOutput.actual, true);
+            } else if (operation == Operation.REM) {
+                promote = AnalyzerCaster.promoteNumeric(leftOutput.actual, rightOutput.actual, true);
+            } else if (operation == Operation.ADD) {
+                promote = AnalyzerCaster.promoteAdd(leftOutput.actual, rightOutput.actual);
+            } else if (operation == Operation.SUB) {
+                promote = AnalyzerCaster.promoteNumeric(leftOutput.actual, rightOutput.actual, true);
+            } else if (operation == Operation.LSH) {
+                promote = AnalyzerCaster.promoteNumeric(leftOutput.actual, false);
+                shiftDistance = AnalyzerCaster.promoteNumeric(rightOutput.actual, false);
+                shift = true;
+            } else if (operation == Operation.RSH) {
+                promote = AnalyzerCaster.promoteNumeric(leftOutput.actual, false);
+                shiftDistance = AnalyzerCaster.promoteNumeric(rightOutput.actual, false);
+                shift = true;
+            } else if (operation == Operation.USH) {
+                promote = AnalyzerCaster.promoteNumeric(leftOutput.actual, false);
+                shiftDistance = AnalyzerCaster.promoteNumeric(rightOutput.actual, false);
+                shift = true;
+            } else if (operation == Operation.BWAND) {
+                promote = AnalyzerCaster.promoteXor(leftOutput.actual, rightOutput.actual);
+            } else if (operation == Operation.XOR) {
+                promote = AnalyzerCaster.promoteXor(leftOutput.actual, rightOutput.actual);
+            } else if (operation == Operation.BWOR) {
+                promote = AnalyzerCaster.promoteXor(leftOutput.actual, rightOutput.actual);
+            } else {
+                throw createError(new IllegalStateException("Illegal tree structure."));
+            }
 
-        if (promote == null || (shift && shiftDistance == null)) {
-            throw createError(new ClassCastException("Cannot apply compound assignment " +
-                "[" + operation.symbol + "=] to types [" + lhs.actual + "] and [" + rhs.actual + "]."));
-        }
+            if (promote == null || (shift && shiftDistance == null)) {
+                throw createError(new ClassCastException("Cannot apply compound assignment " +
+                        "[" + operation.symbol + "=] to types [" + leftOutput.actual + "] and [" + rightOutput.actual + "]."));
+            }
 
-        cat = operation == Operation.ADD && promote == String.class;
+            cat = operation == Operation.ADD && promote == String.class;
 
-        if (cat) {
-            if (rhs instanceof EBinary && ((EBinary)rhs).operation == Operation.ADD && rhs.actual == String.class) {
-                ((EBinary)rhs).cat = true;
+            if (cat) {
+                if (rhs instanceof EBinary && ((EBinary)rhs).operation == Operation.ADD && rightOutput.actual == String.class) {
+                    ((EBinary)rhs).cat = true;
+                }
             }
 
-            rhs.expected = rhs.actual;
-        } else if (shift) {
-            if (promote == def.class) {
-                // shifts are promoted independently, but for the def type, we need object.
-                rhs.expected = promote;
-            } else if (shiftDistance == long.class) {
-                rhs.expected = int.class;
-                rhs.explicit = true;
+            if (shift) {
+                if (promote == def.class) {
+                    // shifts are promoted independently, but for the def type, we need object.
+                    rhs.input.expected = promote;
+                } else if (shiftDistance == long.class) {
+                    rhs.input.expected = int.class;
+                    rhs.input.explicit = true;
+                } else {
+                    rhs.input.expected = shiftDistance;
+                }
             } else {
-                rhs.expected = shiftDistance;
+                rhs.input.expected = promote;
             }
-        } else {
-            rhs.expected = promote;
-        }
 
-        rhs.cast();
+            rhs.cast();
 
-        there = AnalyzerCaster.getLegalCast(location, lhs.actual, promote, false, false);
-        back = AnalyzerCaster.getLegalCast(location, promote, lhs.actual, true, false);
+            there = AnalyzerCaster.getLegalCast(location, leftOutput.actual, promote, false, false);
+            back = AnalyzerCaster.getLegalCast(location, promote, leftOutput.actual, true, false);
 
-        this.statement = true;
-        this.actual = read ? lhs.actual : void.class;
-    }
 
-    private void analyzeSimple(ScriptRoot scriptRoot, Scope scope) {
-        AStoreable lhs = (AStoreable)this.lhs;
+        } else if (rhs != null) {
+            AStoreable lhs = (AStoreable)this.lhs;
 
-        // If the lhs node is a def optimized node we update the actual type to remove the need for a cast.
-        if (lhs.isDefOptimized()) {
-            rhs.analyze(scriptRoot, scope);
+            // If the lhs node is a def optimized node we update the actual type to remove the need for a cast.
+            if (lhs.isDefOptimized()) {
+                rightOutput = rhs.analyze(scriptRoot, scope, new Input());
 
-            if (rhs.actual == void.class) {
-                throw createError(new IllegalArgumentException("Right-hand side cannot be a [void] type for assignment."));
+                if (rightOutput.actual == void.class) {
+                    throw createError(new IllegalArgumentException("Right-hand side cannot be a [void] type for assignment."));
+                }
+
+                rhs.input.expected = rightOutput.actual;
+                lhs.updateActual(rightOutput.actual);
+            // Otherwise, we must adapt the rhs type to the lhs type with a cast.
+            } else {
+                Input rightInput = new Input();
+                rightInput.expected = leftOutput.actual;
+                rhs.analyze(scriptRoot, scope, rightInput);
             }
 
-            rhs.expected = rhs.actual;
-            lhs.updateActual(rhs.actual);
-        // Otherwise, we must adapt the rhs type to the lhs type with a cast.
+            rhs.cast();
         } else {
-            rhs.expected = lhs.actual;
-            rhs.analyze(scriptRoot, scope);
+            throw new IllegalStateException("Illegal tree structure.");
         }
 
-        rhs.cast();
+        output.statement = true;
+        output.actual = input.read ? leftOutput.actual : void.class;
 
-        this.statement = true;
-        this.actual = read ? lhs.actual : void.class;
+        return output;
     }
 
     /**
@@ -236,12 +230,12 @@ public final class EAssignment extends AExpression {
         assignmentNode.setRightNode(rhs.cast(rhs.write(classNode)));
 
         assignmentNode.setLocation(location);
-        assignmentNode.setExpressionType(actual);
+        assignmentNode.setExpressionType(output.actual);
         assignmentNode.setCompoundType(promote);
         assignmentNode.setPre(pre);
         assignmentNode.setPost(post);
         assignmentNode.setOperation(operation);
-        assignmentNode.setRead(read);
+        assignmentNode.setRead(input.read);
         assignmentNode.setCat(cat);
         assignmentNode.setThere(there);
         assignmentNode.setBack(back);

+ 61 - 389
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBinary.java

@@ -55,415 +55,87 @@ public final class EBinary extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        originallyExplicit = explicit;
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
 
-        if (operation == Operation.MUL) {
-            analyzeMul(scriptRoot, scope);
-        } else if (operation == Operation.DIV) {
-            analyzeDiv(scriptRoot, scope);
-        } else if (operation == Operation.REM) {
-            analyzeRem(scriptRoot, scope);
-        } else if (operation == Operation.ADD) {
-            analyzeAdd(scriptRoot, scope);
-        } else if (operation == Operation.SUB) {
-            analyzeSub(scriptRoot, scope);
-        } else if (operation == Operation.FIND) {
-            analyzeRegexOp(scriptRoot, scope);
-        } else if (operation == Operation.MATCH) {
-            analyzeRegexOp(scriptRoot, scope);
-        } else if (operation == Operation.LSH) {
-            analyzeLSH(scriptRoot, scope);
-        } else if (operation == Operation.RSH) {
-            analyzeRSH(scriptRoot, scope);
-        } else if (operation == Operation.USH) {
-            analyzeUSH(scriptRoot, scope);
-        } else if (operation == Operation.BWAND) {
-            analyzeBWAnd(scriptRoot, scope);
-        } else if (operation == Operation.XOR) {
-            analyzeXor(scriptRoot, scope);
-        } else if (operation == Operation.BWOR) {
-            analyzeBWOr(scriptRoot, scope);
-        } else {
-            throw createError(new IllegalStateException("Illegal tree structure."));
-        }
-    }
-
-    private void analyzeMul(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        promote = AnalyzerCaster.promoteNumeric(left.actual, right.actual, true);
-
-        if (promote == null) {
-            throw createError(new ClassCastException("Cannot apply multiply [*] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        actual = promote;
-
-        if (promote == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
-            if (expected != null) {
-                actual = expected;
-            }
-        } else {
-            left.expected = promote;
-            right.expected = promote;
-        }
-
-        left.cast();
-        right.cast();
-    }
-
-    private void analyzeDiv(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        promote = AnalyzerCaster.promoteNumeric(left.actual, right.actual, true);
-
-        if (promote == null) {
-            throw createError(new ClassCastException("Cannot apply divide [/] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        actual = promote;
-
-        if (promote == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
-
-            if (expected != null) {
-                actual = expected;
-            }
-        } else {
-            left.expected = promote;
-            right.expected = promote;
-        }
-
-        left.cast();
-        right.cast();
-    }
-
-    private void analyzeRem(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        promote = AnalyzerCaster.promoteNumeric(left.actual, right.actual, true);
-
-        if (promote == null) {
-            throw createError(new ClassCastException("Cannot apply remainder [%] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        actual = promote;
-
-        if (promote == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
-
-            if (expected != null) {
-                actual = expected;
-            }
-        } else {
-            left.expected = promote;
-            right.expected = promote;
-        }
-
-        left.cast();
-        right.cast();
-    }
-
-    private void analyzeAdd(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        promote = AnalyzerCaster.promoteAdd(left.actual, right.actual);
-
-        if (promote == null) {
-            throw createError(new ClassCastException("Cannot apply add [+] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        actual = promote;
-
-        if (promote == String.class) {
-            left.expected = left.actual;
-
-            if (left instanceof EBinary && ((EBinary)left).operation == Operation.ADD && left.actual == String.class) {
-                ((EBinary)left).cat = true;
-            }
-
-            right.expected = right.actual;
-
-            if (right instanceof EBinary && ((EBinary)right).operation == Operation.ADD && right.actual == String.class) {
-                ((EBinary)right).cat = true;
-            }
-        } else if (promote == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
-
-            if (expected != null) {
-                actual = expected;
-            }
-        } else {
-            left.expected = promote;
-            right.expected = promote;
-        }
-
-        left.cast();
-        right.cast();
-    }
-
-    private void analyzeSub(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        promote = AnalyzerCaster.promoteNumeric(left.actual, right.actual, true);
-
-        if (promote == null) {
-            throw createError(new ClassCastException("Cannot apply subtract [-] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        actual = promote;
-
-        if (promote == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
-
-            if (expected != null) {
-                actual = expected;
-            }
-        } else {
-            left.expected = promote;
-            right.expected = promote;
-        }
-
-        left.cast();
-        right.cast();
-    }
+        originallyExplicit = input.explicit;
 
-    private void analyzeRegexOp(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
+        Output leftOutput = left.analyze(scriptRoot, scope, new Input());
+        Output rightOutput = right.analyze(scriptRoot, scope, new Input());
 
-        left.expected = String.class;
-        right.expected = Pattern.class;
-
-        left.cast();
-        right.cast();
-
-        promote = boolean.class;
-        actual = boolean.class;
-    }
-
-    private void analyzeLSH(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        Class<?> lhspromote = AnalyzerCaster.promoteNumeric(left.actual, false);
-        Class<?> rhspromote = AnalyzerCaster.promoteNumeric(right.actual, false);
-
-        if (lhspromote == null || rhspromote == null) {
-            throw createError(new ClassCastException("Cannot apply left shift [<<] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        actual = promote = lhspromote;
-        shiftDistance = rhspromote;
-
-        if (lhspromote == def.class || rhspromote == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
-
-            if (expected != null) {
-                actual = expected;
-            }
+        if (operation == Operation.FIND || operation == Operation.MATCH) {
+            left.input.expected = String.class;
+            right.input.expected = Pattern.class;
+            promote = boolean.class;
+            output.actual = boolean.class;
         } else {
-            left.expected = lhspromote;
-
-            if (rhspromote == long.class) {
-                right.expected = int.class;
-                right.explicit = true;
+            if (operation == Operation.MUL || operation == Operation.DIV || operation == Operation.REM || operation == Operation.SUB) {
+                promote = AnalyzerCaster.promoteNumeric(leftOutput.actual, rightOutput.actual, true);
+            } else if (operation == Operation.ADD) {
+                promote = AnalyzerCaster.promoteAdd(leftOutput.actual, rightOutput.actual);
+            } else if (operation == Operation.LSH || operation == Operation.RSH || operation == Operation.USH) {
+                promote = AnalyzerCaster.promoteNumeric(leftOutput.actual, false);
+                shiftDistance = AnalyzerCaster.promoteNumeric(rightOutput.actual, false);
+
+                if (shiftDistance == null) {
+                    promote = null;
+                }
+            } else if (operation == Operation.BWOR || operation == Operation.BWAND) {
+                promote = AnalyzerCaster.promoteNumeric(leftOutput.actual, rightOutput.actual, false);
+            } else if (operation == Operation.XOR) {
+                promote = AnalyzerCaster.promoteXor(leftOutput.actual, rightOutput.actual);
             } else {
-                right.expected = rhspromote;
+                throw createError(new IllegalStateException("unexpected binary operation [" + operation.name + "]"));
             }
-        }
-
-        left.cast();
-        right.cast();
-    }
-
-    private void analyzeRSH(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
 
-        Class<?> lhspromote = AnalyzerCaster.promoteNumeric(left.actual, false);
-        Class<?> rhspromote = AnalyzerCaster.promoteNumeric(right.actual, false);
-
-        if (lhspromote == null || rhspromote == null) {
-            throw createError(new ClassCastException("Cannot apply right shift [>>] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        actual = promote = lhspromote;
-        shiftDistance = rhspromote;
-
-        if (lhspromote == def.class || rhspromote == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
-
-            if (expected != null) {
-                actual = expected;
+            if (promote == null) {
+                throw createError(new ClassCastException("cannot apply the " + operation.name + " operator " +
+                        "[" + operation.symbol + "] to the types " +
+                        "[" + PainlessLookupUtility.typeToCanonicalTypeName(leftOutput.actual) + "] and " +
+                        "[" + PainlessLookupUtility.typeToCanonicalTypeName(rightOutput.actual) + "]"));
             }
-        } else {
-            left.expected = lhspromote;
 
-            if (rhspromote == long.class) {
-                right.expected = int.class;
-                right.explicit = true;
-            } else {
-                right.expected = rhspromote;
-            }
-        }
+            output.actual = promote;
 
-        left.cast();
-        right.cast();
-    }
-
-    private void analyzeUSH(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
+            if (operation == Operation.ADD && promote == String.class) {
+                left.input.expected = leftOutput.actual;
+                right.input.expected = rightOutput.actual;
 
-        Class<?> lhspromote = AnalyzerCaster.promoteNumeric(left.actual, false);
-        Class<?> rhspromote = AnalyzerCaster.promoteNumeric(right.actual, false);
+                if (left instanceof EBinary && ((EBinary) left).operation == Operation.ADD && leftOutput.actual == String.class) {
+                    ((EBinary) left).cat = true;
+                }
 
-        actual = promote = lhspromote;
-        shiftDistance = rhspromote;
-
-        if (lhspromote == null || rhspromote == null) {
-            throw createError(new ClassCastException("Cannot apply unsigned shift [>>>] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        if (lhspromote == def.class || rhspromote == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
-
-            if (expected != null) {
-                actual = expected;
-            }
-        } else {
-            left.expected = lhspromote;
+                if (right instanceof EBinary && ((EBinary) right).operation == Operation.ADD && rightOutput.actual == String.class) {
+                    ((EBinary) right).cat = true;
+                }
+            } else if (promote == def.class || shiftDistance != null && shiftDistance == def.class) {
+                left.input.expected = leftOutput.actual;
+                right.input.expected = rightOutput.actual;
 
-            if (rhspromote == long.class) {
-                right.expected = int.class;
-                right.explicit = true;
+                if (input.expected != null) {
+                    output.actual = input.expected;
+                }
             } else {
-                right.expected = rhspromote;
-            }
-        }
-
-        left.cast();
-        right.cast();
-    }
-
-    private void analyzeBWAnd(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        promote = AnalyzerCaster.promoteNumeric(left.actual, right.actual, false);
-
-        if (promote == null) {
-            throw createError(new ClassCastException("Cannot apply and [&] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        actual = promote;
-
-        if (promote == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
-
-            if (expected != null) {
-                actual = expected;
-            }
-        } else {
-            left.expected = promote;
-            right.expected = promote;
-        }
-
-        left.cast();
-        right.cast();
-    }
-
-    private void analyzeXor(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        promote = AnalyzerCaster.promoteXor(left.actual, right.actual);
-
-        if (promote == null) {
-            throw createError(new ClassCastException("Cannot apply xor [^] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
+                left.input.expected = promote;
 
-        actual = promote;
-
-        if (promote == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
-            if (expected != null) {
-                actual = expected;
+                if (operation == Operation.LSH || operation == Operation.RSH || operation == Operation.USH) {
+                    if (shiftDistance == long.class) {
+                        right.input.expected = int.class;
+                        right.input.explicit = true;
+                    } else {
+                        right.input.expected = shiftDistance;
+                    }
+                } else {
+                    right.input.expected = promote;
+                }
             }
-        } else {
-            left.expected = promote;
-            right.expected = promote;
         }
 
         left.cast();
         right.cast();
-    }
-
-    private void analyzeBWOr(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        promote = AnalyzerCaster.promoteNumeric(left.actual, right.actual, false);
 
-        if (promote == null) {
-            throw createError(new ClassCastException("Cannot apply or [|] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        actual = promote;
-
-        if (promote == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
-            if (expected != null) {
-                actual = expected;
-            }
-        } else {
-            left.expected = promote;
-            right.expected = promote;
-        }
-
-        left.cast();
-        right.cast();
+        return output;
     }
 
     @Override
@@ -474,7 +146,7 @@ public final class EBinary extends AExpression {
         binaryMathNode.setRightNode(right.cast(right.write(classNode)));
 
         binaryMathNode.setLocation(location);
-        binaryMathNode.setExpressionType(actual);
+        binaryMathNode.setExpressionType(output.actual);
         binaryMathNode.setBinaryType(promote);
         binaryMathNode.setShiftType(shiftDistance);
         binaryMathNode.setOperation(operation);

+ 14 - 7
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBool.java

@@ -46,16 +46,23 @@ public final class EBool extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        left.expected = boolean.class;
-        left.analyze(scriptRoot, scope);
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
+        Input leftInput = new Input();
+        leftInput.expected = boolean.class;
+        left.analyze(scriptRoot, scope, leftInput);
         left.cast();
 
-        right.expected = boolean.class;
-        right.analyze(scriptRoot, scope);
+        Input rightInput = new Input();
+        rightInput.expected = boolean.class;
+        right.analyze(scriptRoot, scope, rightInput);
         right.cast();
 
-        actual = boolean.class;
+        output.actual = boolean.class;
+
+        return output;
     }
 
     @Override
@@ -66,7 +73,7 @@ public final class EBool extends AExpression {
         booleanNode.setRightNode(right.cast(right.write(classNode)));
 
         booleanNode.setLocation(location);
-        booleanNode.setExpressionType(actual);
+        booleanNode.setExpressionType(output.actual);
         booleanNode.setOperation(operation);
 
         return booleanNode;

+ 9 - 4
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EBoolean.java

@@ -40,19 +40,24 @@ public final class EBoolean extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        if (!read) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
+        if (input.read == false) {
             throw createError(new IllegalArgumentException("Must read from constant [" + constant + "]."));
         }
 
-        actual = boolean.class;
+        output.actual = boolean.class;
+
+        return output;
     }
 
     @Override
     ExpressionNode write(ClassNode classNode) {
         ConstantNode constantNode = new ConstantNode();
         constantNode.setLocation(location);
-        constantNode.setExpressionType(actual);
+        constantNode.setExpressionType(output.actual);
         constantNode.setConstant(constant);
 
         return constantNode;

+ 16 - 10
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java

@@ -58,7 +58,10 @@ public final class ECallLocal extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
         localFunction = scriptRoot.getFunctionTable().getFunction(name, arguments.size());
 
         // user cannot call internal functions, reset to null if an internal function is found
@@ -112,21 +115,21 @@ public final class ECallLocal extends AExpression {
 
         if (localFunction != null) {
             typeParameters = new ArrayList<>(localFunction.getTypeParameters());
-            actual = localFunction.getReturnType();
+            output.actual = localFunction.getReturnType();
         } else if (importedMethod != null) {
             scriptRoot.markNonDeterministic(importedMethod.annotations.containsKey(NonDeterministicAnnotation.class));
             typeParameters = new ArrayList<>(importedMethod.typeParameters);
-            actual = importedMethod.returnType;
+            output.actual = importedMethod.returnType;
         } else if (classBinding != null) {
             scriptRoot.markNonDeterministic(classBinding.annotations.containsKey(NonDeterministicAnnotation.class));
             typeParameters = new ArrayList<>(classBinding.typeParameters);
-            actual = classBinding.returnType;
+            output.actual = classBinding.returnType;
             bindingName = scriptRoot.getNextSyntheticName("class_binding");
             scriptRoot.getClassNode().addField(new SField(location,
                     Modifier.PRIVATE, bindingName, classBinding.javaConstructor.getDeclaringClass()));
         } else if (instanceBinding != null) {
             typeParameters = new ArrayList<>(instanceBinding.typeParameters);
-            actual = instanceBinding.returnType;
+            output.actual = instanceBinding.returnType;
             bindingName = scriptRoot.getNextSyntheticName("instance_binding");
             scriptRoot.getClassNode().addField(new SField(location, Modifier.STATIC | Modifier.PUBLIC,
                     bindingName, instanceBinding.targetInstance.getClass()));
@@ -141,13 +144,16 @@ public final class ECallLocal extends AExpression {
         for (int argument = 0; argument < arguments.size(); ++argument) {
             AExpression expression = arguments.get(argument);
 
-            expression.expected = typeParameters.get(argument + classBindingOffset);
-            expression.internal = true;
-            expression.analyze(scriptRoot, scope);
+            Input expressionInput = new Input();
+            expressionInput.expected = typeParameters.get(argument + classBindingOffset);
+            expressionInput.internal = true;
+            expression.analyze(scriptRoot, scope, expressionInput);
             expression.cast();
         }
 
-        statement = true;
+        output.statement = true;
+
+        return output;
     }
 
     @Override
@@ -159,7 +165,7 @@ public final class ECallLocal extends AExpression {
         }
 
         memberCallNode.setLocation(location);
-        memberCallNode.setExpressionType(actual);
+        memberCallNode.setExpressionType(output.actual);
         memberCallNode.setLocalFunction(localFunction);
         memberCallNode.setImportedMethod(importedMethod);
         memberCallNode.setClassBinding(classBinding);

+ 11 - 6
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECapturingFunctionRef.java

@@ -51,9 +51,12 @@ public final class ECapturingFunctionRef extends AExpression implements ILambda
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
         captured = scope.getVariable(location, variable);
-        if (expected == null) {
+        if (input.expected == null) {
             if (captured.getType() == def.class) {
                 // dynamic implementation
                 defPointer = "D" + variable + "." + call + ",1";
@@ -61,16 +64,18 @@ public final class ECapturingFunctionRef extends AExpression implements ILambda
                 // typed implementation
                 defPointer = "S" + captured.getCanonicalTypeName() + "." + call + ",1";
             }
-            actual = String.class;
+            output.actual = String.class;
         } else {
             defPointer = null;
             // static case
             if (captured.getType() != def.class) {
                 ref = FunctionRef.create(scriptRoot.getPainlessLookup(), scriptRoot.getFunctionTable(), location,
-                        expected, captured.getCanonicalTypeName(), call, 1);
+                        input.expected, captured.getCanonicalTypeName(), call, 1);
             }
-            actual = expected;
+            output.actual = input.expected;
         }
+
+        return output;
     }
 
     @Override
@@ -78,7 +83,7 @@ public final class ECapturingFunctionRef extends AExpression implements ILambda
         CapturingFuncRefNode capturingFuncRefNode = new CapturingFuncRefNode();
 
         capturingFuncRefNode.setLocation(location);
-        capturingFuncRefNode.setExpressionType(actual);
+        capturingFuncRefNode.setExpressionType(output.actual);
         capturingFuncRefNode.setCapturedName(captured.getName());
         capturingFuncRefNode.setName(call);
         capturingFuncRefNode.setPointer(defPointer);

+ 25 - 218
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EComp.java

@@ -51,240 +51,47 @@ public final class EComp extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        if (operation == Operation.EQ) {
-            analyzeEq(scriptRoot, scope);
-        } else if (operation == Operation.EQR) {
-            analyzeEqR(scriptRoot, scope);
-        } else if (operation == Operation.NE) {
-            analyzeNE(scriptRoot, scope);
-        } else if (operation == Operation.NER) {
-            analyzeNER(scriptRoot, scope);
-        } else if (operation == Operation.GTE) {
-            analyzeGTE(scriptRoot, scope);
-        } else if (operation == Operation.GT) {
-            analyzeGT(scriptRoot, scope);
-        } else if (operation == Operation.LTE) {
-            analyzeLTE(scriptRoot, scope);
-        } else if (operation == Operation.LT) {
-            analyzeLT(scriptRoot, scope);
-        } else {
-            throw createError(new IllegalStateException("Illegal tree structure."));
-        }
-    }
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
 
-    private void analyzeEq(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        promotedType = AnalyzerCaster.promoteEquality(left.actual, right.actual);
-
-        if (promotedType == null) {
-            throw createError(new ClassCastException("Cannot apply equals [==] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
+        Output leftOutput = left.analyze(scriptRoot, scope, new Input());
+        Output rightOutput = right.analyze(scriptRoot, scope, new Input());
 
-        if (promotedType == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
+        if (operation == Operation.EQ || operation == Operation.EQR || operation == Operation.NE || operation == Operation.NER) {
+            promotedType = AnalyzerCaster.promoteEquality(leftOutput.actual, rightOutput.actual);
+        } else if (operation == Operation.GT || operation == Operation.GTE || operation == Operation.LT || operation == Operation.LTE) {
+            promotedType = AnalyzerCaster.promoteNumeric(leftOutput.actual, rightOutput.actual, true);
         } else {
-            left.expected = promotedType;
-            right.expected = promotedType;
-        }
-
-        if (left instanceof ENull && right instanceof ENull) {
-            throw createError(new IllegalArgumentException("Extraneous comparison of null constants."));
+            throw createError(new IllegalStateException("unexpected binary operation [" + operation.name + "]"));
         }
 
-        left.cast();
-        right.cast();
-
-        actual = boolean.class;
-    }
-
-    private void analyzeEqR(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        promotedType = AnalyzerCaster.promoteEquality(left.actual, right.actual);
-
         if (promotedType == null) {
-            throw createError(new ClassCastException("Cannot apply reference equals [===] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        left.expected = promotedType;
-        right.expected = promotedType;
-
-        if (left instanceof ENull && right instanceof ENull) {
-            throw createError(new IllegalArgumentException("Extraneous comparison of null constants."));
+            throw createError(new ClassCastException("cannot apply the " + operation.name + " operator " +
+                    "[" + operation.symbol + "] to the types " +
+                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(leftOutput.actual) + "] and " +
+                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(rightOutput.actual) + "]"));
         }
 
-        left.cast();
-        right.cast();
-
-        actual = boolean.class;
-    }
-
-    private void analyzeNE(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        promotedType = AnalyzerCaster.promoteEquality(left.actual, right.actual);
-
-        if (promotedType == null) {
-            throw createError(new ClassCastException("Cannot apply not equals [!=] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        if (promotedType == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
+        if (operation != Operation.EQR && operation != Operation.NER && promotedType == def.class) {
+            left.input.expected = leftOutput.actual;
+            right.input.expected = rightOutput.actual;
         } else {
-            left.expected = promotedType;
-            right.expected = promotedType;
+            left.input.expected = promotedType;
+            right.input.expected = promotedType;
         }
 
-        if (left instanceof ENull && right instanceof ENull) {
-            throw createError(new IllegalArgumentException("Extraneous comparison of null constants."));
+        if ((operation == Operation.EQ || operation == Operation.EQR || operation == Operation.NE || operation == Operation.NER)
+                && left instanceof ENull && right instanceof ENull) {
+            throw createError(new IllegalArgumentException("extraneous comparison of [null] constants"));
         }
 
         left.cast();
         right.cast();
 
-        actual = boolean.class;
-    }
-
-    private void analyzeNER(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        promotedType = AnalyzerCaster.promoteEquality(left.actual, right.actual);
-
-        if (promotedType == null) {
-            throw createError(new ClassCastException("Cannot apply reference not equals [!==] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        left.expected = promotedType;
-        right.expected = promotedType;
-
-        if (left instanceof ENull && right instanceof ENull) {
-            throw createError(new IllegalArgumentException("Extraneous comparison of null constants."));
-        }
-
-        left.cast();
-        right.cast();
-
-        actual = boolean.class;
-    }
-
-    private void analyzeGTE(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        promotedType = AnalyzerCaster.promoteNumeric(left.actual, right.actual, true);
-
-        if (promotedType == null) {
-            throw createError(new ClassCastException("Cannot apply greater than or equals [>=] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        if (promotedType == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
-        } else {
-            left.expected = promotedType;
-            right.expected = promotedType;
-        }
-
-        left.cast();
-        right.cast();
-
-        actual = boolean.class;
-    }
-
-    private void analyzeGT(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        promotedType = AnalyzerCaster.promoteNumeric(left.actual, right.actual, true);
-
-        if (promotedType == null) {
-            throw createError(new ClassCastException("Cannot apply greater than [>] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        if (promotedType == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
-        } else {
-            left.expected = promotedType;
-            right.expected = promotedType;
-        }
-
-        left.cast();
-        right.cast();
-
-        actual = boolean.class;
-    }
-
-    private void analyzeLTE(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        promotedType = AnalyzerCaster.promoteNumeric(left.actual, right.actual, true);
-
-        if (promotedType == null) {
-            throw createError(new ClassCastException("Cannot apply less than or equals [<=] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        if (promotedType == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
-        } else {
-            left.expected = promotedType;
-            right.expected = promotedType;
-        }
-
-        left.cast();
-        right.cast();
-
-        actual = boolean.class;
-    }
-
-    private void analyzeLT(ScriptRoot scriptRoot, Scope variables) {
-        left.analyze(scriptRoot, variables);
-        right.analyze(scriptRoot, variables);
-
-        promotedType = AnalyzerCaster.promoteNumeric(left.actual, right.actual, true);
-
-        if (promotedType == null) {
-            throw createError(new ClassCastException("Cannot apply less than [>=] to types " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]."));
-        }
-
-        if (promotedType == def.class) {
-            left.expected = left.actual;
-            right.expected = right.actual;
-        } else {
-            left.expected = promotedType;
-            right.expected = promotedType;
-        }
-
-        left.cast();
-        right.cast();
+        output.actual = boolean.class;
 
-        actual = boolean.class;
+        return output;
     }
 
     @Override
@@ -295,7 +102,7 @@ public final class EComp extends AExpression {
         comparisonNode.setRightNode(right.cast(right.write(classNode)));
 
         comparisonNode.setLocation(location);
-        comparisonNode.setExpressionType(actual);
+        comparisonNode.setExpressionType(output.actual);
         comparisonNode.setComparisonType(promotedType);
         comparisonNode.setOperation(operation);
 

+ 31 - 21
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EConditional.java

@@ -47,38 +47,48 @@ public final class EConditional extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        condition.expected = boolean.class;
-        condition.analyze(scriptRoot, scope);
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
+        Input conditionInput = new Input();
+        conditionInput.expected = boolean.class;
+        condition.analyze(scriptRoot, scope, conditionInput);
         condition.cast();
 
-        left.expected = expected;
-        left.explicit = explicit;
-        left.internal = internal;
-        right.expected = expected;
-        right.explicit = explicit;
-        right.internal = internal;
-        actual = expected;
+        Input leftInput = new Input();
+        leftInput.expected = input.expected;
+        leftInput.explicit = input.explicit;
+        leftInput.internal = input.internal;
+
+        Input rightInput = new Input();
+        rightInput.expected = input.expected;
+        rightInput.explicit = input.explicit;
+        rightInput.internal = input.internal;
 
-        left.analyze(scriptRoot, scope);
-        right.analyze(scriptRoot, scope);
+        output.actual = input.expected;
 
-        if (expected == null) {
-            Class<?> promote = AnalyzerCaster.promoteConditional(left.actual, right.actual);
+        Output leftOutput = left.analyze(scriptRoot, scope, leftInput);
+        Output rightOutput = right.analyze(scriptRoot, scope, rightInput);
+
+        if (input.expected == null) {
+            Class<?> promote = AnalyzerCaster.promoteConditional(leftOutput.actual, rightOutput.actual);
 
             if (promote == null) {
-                throw createError(new ClassCastException("cannot apply a conditional operator [?:] to the types " +
-                        "[" + PainlessLookupUtility.typeToCanonicalTypeName(left.actual) + "] and " +
-                        "[" + PainlessLookupUtility.typeToCanonicalTypeName(right.actual) + "]"));
+                throw createError(new ClassCastException("cannot apply the conditional operator [?:] to the types " +
+                        "[" + PainlessLookupUtility.typeToCanonicalTypeName(leftOutput.actual) + "] and " +
+                        "[" + PainlessLookupUtility.typeToCanonicalTypeName(rightOutput.actual) + "]"));
             }
 
-            left.expected = promote;
-            right.expected = promote;
-            actual = promote;
+            left.input.expected = promote;
+            right.input.expected = promote;
+            output.actual = promote;
         }
 
         left.cast();
         right.cast();
+
+        return output;
     }
 
     @Override
@@ -90,7 +100,7 @@ public final class EConditional extends AExpression {
         conditionalNode.setConditionNode(condition.cast(condition.write(classNode)));
 
         conditionalNode.setLocation(location);
-        conditionalNode.setExpressionType(actual);
+        conditionalNode.setExpressionType(output.actual);
 
         return conditionalNode;
     }

+ 20 - 12
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EConstant.java

@@ -23,6 +23,7 @@ import org.elasticsearch.painless.Location;
 import org.elasticsearch.painless.Scope;
 import org.elasticsearch.painless.ir.ClassNode;
 import org.elasticsearch.painless.ir.ConstantNode;
+import org.elasticsearch.painless.lookup.PainlessLookupUtility;
 import org.elasticsearch.painless.symbol.ScriptRoot;
 
 /**
@@ -40,35 +41,42 @@ final class EConstant extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
         if (constant instanceof String) {
-            actual = String.class;
+            output.actual = String.class;
         } else if (constant instanceof Double) {
-            actual = double.class;
+            output.actual = double.class;
         } else if (constant instanceof Float) {
-            actual = float.class;
+            output.actual = float.class;
         } else if (constant instanceof Long) {
-            actual = long.class;
+            output.actual = long.class;
         } else if (constant instanceof Integer) {
-            actual = int.class;
+            output.actual = int.class;
         } else if (constant instanceof Character) {
-            actual = char.class;
+            output.actual = char.class;
         } else if (constant instanceof Short) {
-            actual = short.class;
+            output.actual = short.class;
         } else if (constant instanceof Byte) {
-            actual = byte.class;
+            output.actual = byte.class;
         } else if (constant instanceof Boolean) {
-            actual = boolean.class;
+            output.actual = boolean.class;
         } else {
-            throw createError(new IllegalStateException("Illegal tree structure."));
+            throw createError(new IllegalStateException("unexpected type " +
+                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(constant.getClass()) + "] " +
+                    "for constant node"));
         }
+
+        return output;
     }
 
     @Override
     ConstantNode write(ClassNode classNode) {
         ConstantNode constantNode = new ConstantNode();
         constantNode.setLocation(location);
-        constantNode.setExpressionType(actual);
+        constantNode.setExpressionType(output.actual);
         constantNode.setConstant(constant);
 
         return constantNode;

+ 10 - 5
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EDecimal.java

@@ -44,15 +44,18 @@ public final class EDecimal extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        if (!read) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
+        if (input.read == false) {
             throw createError(new IllegalArgumentException("Must read from constant [" + value + "]."));
         }
 
         if (value.endsWith("f") || value.endsWith("F")) {
             try {
                 constant = Float.parseFloat(value.substring(0, value.length() - 1));
-                actual = float.class;
+                output.actual = float.class;
             } catch (NumberFormatException exception) {
                 throw createError(new IllegalArgumentException("Invalid float constant [" + value + "]."));
             }
@@ -63,18 +66,20 @@ public final class EDecimal extends AExpression {
             }
             try {
                 constant = Double.parseDouble(toParse);
-                actual = double.class;
+                output.actual = double.class;
             } catch (NumberFormatException exception) {
                 throw createError(new IllegalArgumentException("Invalid double constant [" + value + "]."));
             }
         }
+
+        return output;
     }
 
     @Override
     ExpressionNode write(ClassNode classNode) {
         ConstantNode constantNode = new ConstantNode();
         constantNode.setLocation(location);
-        constantNode.setExpressionType(actual);
+        constantNode.setExpressionType(output.actual);
         constantNode.setConstant(constant);
 
         return constantNode;

+ 25 - 18
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EElvis.java

@@ -44,19 +44,24 @@ public class EElvis extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        if (expected != null && expected.isPrimitive()) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
+        if (input.expected != null && input.expected.isPrimitive()) {
             throw createError(new IllegalArgumentException("Elvis operator cannot return primitives"));
         }
-        lhs.expected = expected;
-        lhs.explicit = explicit;
-        lhs.internal = internal;
-        rhs.expected = expected;
-        rhs.explicit = explicit;
-        rhs.internal = internal;
-        actual = expected;
-        lhs.analyze(scriptRoot, scope);
-        rhs.analyze(scriptRoot, scope);
+        Input leftInput = new Input();
+        leftInput.expected = input.expected;
+        leftInput.explicit = input.explicit;
+        leftInput.internal = input.internal;
+        Input rightInput = new Input();
+        rightInput.expected = input.expected;
+        rightInput.explicit = input.explicit;
+        rightInput.internal = input.internal;
+        output.actual = input.expected;
+        Output leftOutput = lhs.analyze(scriptRoot, scope, leftInput);
+        Output rightOutput = rhs.analyze(scriptRoot, scope, rightInput);
 
         if (lhs instanceof ENull) {
             throw createError(new IllegalArgumentException("Extraneous elvis operator. LHS is null."));
@@ -68,23 +73,25 @@ public class EElvis extends AExpression {
                 || lhs instanceof EConstant) {
             throw createError(new IllegalArgumentException("Extraneous elvis operator. LHS is a constant."));
         }
-        if (lhs.actual.isPrimitive()) {
+        if (leftOutput.actual.isPrimitive()) {
             throw createError(new IllegalArgumentException("Extraneous elvis operator. LHS is a primitive."));
         }
         if (rhs instanceof ENull) {
             throw createError(new IllegalArgumentException("Extraneous elvis operator. RHS is null."));
         }
 
-        if (expected == null) {
-            Class<?> promote = AnalyzerCaster.promoteConditional(lhs.actual, rhs.actual);
+        if (input.expected == null) {
+            Class<?> promote = AnalyzerCaster.promoteConditional(leftOutput.actual, rightOutput.actual);
 
-            lhs.expected = promote;
-            rhs.expected = promote;
-            actual = promote;
+            lhs.input.expected = promote;
+            rhs.input.expected = promote;
+            output.actual = promote;
         }
 
         lhs.cast();
         rhs.cast();
+
+        return output;
     }
 
     @Override
@@ -95,7 +102,7 @@ public class EElvis extends AExpression {
         elvisNode.setRightNode(rhs.cast(rhs.write(classNode)));
 
         elvisNode.setLocation(location);
-        elvisNode.setExpressionType(actual);
+        elvisNode.setExpressionType(output.actual);
 
         return elvisNode;
     }

+ 12 - 6
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EExplicit.java

@@ -43,17 +43,23 @@ public final class EExplicit extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        actual = scriptRoot.getPainlessLookup().canonicalTypeNameToType(type);
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
 
-        if (actual == null) {
+        output.actual = scriptRoot.getPainlessLookup().canonicalTypeNameToType(type);
+
+        if (output.actual == null) {
             throw createError(new IllegalArgumentException("Not a type [" + type + "]."));
         }
 
-        child.expected = actual;
-        child.explicit = true;
-        child.analyze(scriptRoot, scope);
+        Input childInput = new Input();
+        childInput.expected = output.actual;
+        childInput.explicit = true;
+        child.analyze(scriptRoot, scope, childInput);
         child.cast();
+
+        return output;
     }
 
     @Override

+ 12 - 6
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EFunctionRef.java

@@ -48,16 +48,22 @@ public final class EFunctionRef extends AExpression implements ILambda {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        if (expected == null) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
+        if (input.expected == null) {
             ref = null;
-            actual = String.class;
+            output.actual = String.class;
             defPointer = "S" + type + "." + call + ",0";
         } else {
             defPointer = null;
-            ref = FunctionRef.create(scriptRoot.getPainlessLookup(), scriptRoot.getFunctionTable(), location, expected, type, call, 0);
-            actual = expected;
+            ref = FunctionRef.create(
+                    scriptRoot.getPainlessLookup(), scriptRoot.getFunctionTable(), location, input.expected, type, call, 0);
+            output.actual = input.expected;
         }
+
+        return output;
     }
 
     @Override
@@ -65,7 +71,7 @@ public final class EFunctionRef extends AExpression implements ILambda {
         FuncRefNode funcRefNode = new FuncRefNode();
 
         funcRefNode.setLocation(location);
-        funcRefNode.setExpressionType(actual);
+        funcRefNode.setExpressionType(output.actual);
         funcRefNode.setFuncRef(ref);
 
         return funcRefNode;

+ 15 - 10
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EInstanceof.java

@@ -38,7 +38,7 @@ public final class EInstanceof extends AExpression {
     private final String type;
 
     private Class<?> resolvedType;
-    private Class<?> instanceType;
+    private Class<?> expressionType;
     private boolean primitiveExpression;
 
     public EInstanceof(Location location, AExpression expression, String type) {
@@ -48,7 +48,10 @@ public final class EInstanceof extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
         // ensure the specified type is part of the definition
         Class<?> clazz = scriptRoot.getPainlessLookup().canonicalTypeNameToType(this.type);
 
@@ -61,17 +64,19 @@ public final class EInstanceof extends AExpression {
                 PainlessLookupUtility.typeToJavaType(clazz);
 
         // analyze and cast the expression
-        expression.analyze(scriptRoot, scope);
-        expression.expected = expression.actual;
+        Output expressionOutput = expression.analyze(scriptRoot, scope, new Input());
+        expression.input.expected = expressionOutput.actual;
         expression.cast();
 
         // record if the expression returns a primitive
-        primitiveExpression = expression.actual.isPrimitive();
+        primitiveExpression = expressionOutput.actual.isPrimitive();
         // map to wrapped type for primitive types
-        instanceType = expression.actual.isPrimitive() ?
-            PainlessLookupUtility.typeToBoxedType(expression.actual) : PainlessLookupUtility.typeToJavaType(clazz);
+        expressionType = expressionOutput.actual.isPrimitive() ?
+            PainlessLookupUtility.typeToBoxedType(expressionOutput.actual) : PainlessLookupUtility.typeToJavaType(clazz);
+
+        output.actual = boolean.class;
 
-        actual = boolean.class;
+        return output;
     }
 
     @Override
@@ -81,8 +86,8 @@ public final class EInstanceof extends AExpression {
         instanceofNode.setChildNode(expression.cast(expression.write(classNode)));
 
         instanceofNode.setLocation(location);
-        instanceofNode.setExpressionType(actual);
-        instanceofNode.setInstanceType(instanceType);
+        instanceofNode.setExpressionType(output.actual);
+        instanceofNode.setInstanceType(expressionType);
         instanceofNode.setResolvedType(resolvedType);
         instanceofNode.setPrimitiveResult(primitiveExpression);
 

+ 15 - 10
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ELambda.java

@@ -91,11 +91,14 @@ public final class ELambda extends AExpression implements ILambda {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
         List<Class<?>> typeParameters = new ArrayList<>();
         PainlessMethod interfaceMethod;
         // inspect the target first, set interface method if we know it.
-        if (expected == null) {
+        if (input.expected == null) {
             interfaceMethod = null;
             // we don't know anything: treat as def
             returnType = def.class;
@@ -117,15 +120,15 @@ public final class ELambda extends AExpression implements ILambda {
 
         } else {
             // we know the method statically, infer return type and any unknown/def types
-            interfaceMethod = scriptRoot.getPainlessLookup().lookupFunctionalInterfacePainlessMethod(expected);
+            interfaceMethod = scriptRoot.getPainlessLookup().lookupFunctionalInterfacePainlessMethod(input.expected);
             if (interfaceMethod == null) {
                 throw createError(new IllegalArgumentException("Cannot pass lambda to " +
-                        "[" + PainlessLookupUtility.typeToCanonicalTypeName(expected) + "], not a functional interface"));
+                        "[" + PainlessLookupUtility.typeToCanonicalTypeName(input.expected) + "], not a functional interface"));
             }
             // check arity before we manipulate parameters
             if (interfaceMethod.typeParameters.size() != paramTypeStrs.size())
                 throw new IllegalArgumentException("Incorrect number of parameters for [" + interfaceMethod.javaMethod.getName() +
-                        "] in [" + PainlessLookupUtility.typeToCanonicalTypeName(expected) + "]");
+                        "] in [" + PainlessLookupUtility.typeToCanonicalTypeName(input.expected) + "]");
             // for method invocation, its allowed to ignore the return value
             if (interfaceMethod.returnType == void.class) {
                 returnType = def.class;
@@ -187,16 +190,18 @@ public final class ELambda extends AExpression implements ILambda {
         scriptRoot.getFunctionTable().addFunction(name, returnType, this.typeParameters, true, true);
 
         // setup method reference to synthetic method
-        if (expected == null) {
+        if (input.expected == null) {
             ref = null;
-            actual = String.class;
+            output.actual = String.class;
             defPointer = "Sthis." + name + "," + captures.size();
         } else {
             defPointer = null;
             ref = FunctionRef.create(scriptRoot.getPainlessLookup(), scriptRoot.getFunctionTable(),
-                    location, expected, "this", name, captures.size());
-            actual = expected;
+                    location, input.expected, "this", name, captures.size());
+            output.actual = input.expected;
         }
+
+        return output;
     }
 
     @Override
@@ -220,7 +225,7 @@ public final class ELambda extends AExpression implements ILambda {
         LambdaNode lambdaNode = new LambdaNode();
 
         lambdaNode.setLocation(location);
-        lambdaNode.setExpressionType(actual);
+        lambdaNode.setExpressionType(output.actual);
         lambdaNode.setFuncRef(ref);
 
         for (Variable capture : captures) {

+ 17 - 11
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EListInit.java

@@ -49,34 +49,40 @@ public final class EListInit extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        if (!read) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
+        if (input.read == false) {
             throw createError(new IllegalArgumentException("Must read from list initializer."));
         }
 
-        actual = ArrayList.class;
+        output.actual = ArrayList.class;
 
-        constructor = scriptRoot.getPainlessLookup().lookupPainlessConstructor(actual, 0);
+        constructor = scriptRoot.getPainlessLookup().lookupPainlessConstructor(output.actual, 0);
 
         if (constructor == null) {
             throw createError(new IllegalArgumentException(
-                    "constructor [" + typeToCanonicalTypeName(actual) + ", <init>/0] not found"));
+                    "constructor [" + typeToCanonicalTypeName(output.actual) + ", <init>/0] not found"));
         }
 
-        method = scriptRoot.getPainlessLookup().lookupPainlessMethod(actual, false, "add", 1);
+        method = scriptRoot.getPainlessLookup().lookupPainlessMethod(output.actual, false, "add", 1);
 
         if (method == null) {
-            throw createError(new IllegalArgumentException("method [" + typeToCanonicalTypeName(actual) + ", add/1] not found"));
+            throw createError(new IllegalArgumentException("method [" + typeToCanonicalTypeName(output.actual) + ", add/1] not found"));
         }
 
         for (int index = 0; index < values.size(); ++index) {
             AExpression expression = values.get(index);
 
-            expression.expected = def.class;
-            expression.internal = true;
-            expression.analyze(scriptRoot, scope);
+            Input expressionInput = new Input();
+            expressionInput.expected = def.class;
+            expressionInput.internal = true;
+            expression.analyze(scriptRoot, scope, expressionInput);
             expression.cast();
         }
+
+        return output;
     }
 
     @Override
@@ -88,7 +94,7 @@ public final class EListInit extends AExpression {
         }
 
         listInitializationNode.setLocation(location);
-        listInitializationNode.setExpressionType(actual);
+        listInitializationNode.setExpressionType(output.actual);
         listInitializationNode.setConstructor(constructor);
         listInitializationNode.setMethod(method);
 

+ 21 - 14
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EMapInit.java

@@ -51,24 +51,27 @@ public final class EMapInit extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        if (!read) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
+        if (input.read == false) {
             throw createError(new IllegalArgumentException("Must read from map initializer."));
         }
 
-        actual = HashMap.class;
+        output.actual = HashMap.class;
 
-        constructor = scriptRoot.getPainlessLookup().lookupPainlessConstructor(actual, 0);
+        constructor = scriptRoot.getPainlessLookup().lookupPainlessConstructor(output.actual, 0);
 
         if (constructor == null) {
             throw createError(new IllegalArgumentException(
-                    "constructor [" + typeToCanonicalTypeName(actual) + ", <init>/0] not found"));
+                    "constructor [" + typeToCanonicalTypeName(output.actual) + ", <init>/0] not found"));
         }
 
-        method = scriptRoot.getPainlessLookup().lookupPainlessMethod(actual, false, "put", 2);
+        method = scriptRoot.getPainlessLookup().lookupPainlessMethod(output.actual, false, "put", 2);
 
         if (method == null) {
-            throw createError(new IllegalArgumentException("method [" + typeToCanonicalTypeName(actual) + ", put/2] not found"));
+            throw createError(new IllegalArgumentException("method [" + typeToCanonicalTypeName(output.actual) + ", put/2] not found"));
         }
 
         if (keys.size() != values.size()) {
@@ -78,20 +81,24 @@ public final class EMapInit extends AExpression {
         for (int index = 0; index < keys.size(); ++index) {
             AExpression expression = keys.get(index);
 
-            expression.expected = def.class;
-            expression.internal = true;
-            expression.analyze(scriptRoot, scope);
+            Input expressionInput = new Input();
+            expressionInput.expected = def.class;
+            expressionInput.internal = true;
+            expression.analyze(scriptRoot, scope, expressionInput);
             expression.cast();
         }
 
         for (int index = 0; index < values.size(); ++index) {
             AExpression expression = values.get(index);
 
-            expression.expected = def.class;
-            expression.internal = true;
-            expression.analyze(scriptRoot, scope);
+            Input expressionInput = new Input();
+            expressionInput.expected = def.class;
+            expressionInput.internal = true;
+            expression.analyze(scriptRoot, scope, expressionInput);
             expression.cast();
         }
+
+        return output;
     }
 
     @Override
@@ -105,7 +112,7 @@ public final class EMapInit extends AExpression {
         }
 
         mapInitializationNode.setLocation(location);
-        mapInitializationNode.setExpressionType(actual);
+        mapInitializationNode.setExpressionType(output.actual);
         mapInitializationNode.setConstructor(constructor);
         mapInitializationNode.setMethod(method);
 

+ 13 - 7
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewArray.java

@@ -46,8 +46,11 @@ public final class ENewArray extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        if (!read) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
+        if (input.read == false) {
              throw createError(new IllegalArgumentException("A newly created array must be read from."));
         }
 
@@ -60,13 +63,16 @@ public final class ENewArray extends AExpression {
         for (int argument = 0; argument < arguments.size(); ++argument) {
             AExpression expression = arguments.get(argument);
 
-            expression.expected = initialize ? clazz.getComponentType() : int.class;
-            expression.internal = true;
-            expression.analyze(scriptRoot, scope);
+            Input expressionInput = new Input();
+            expressionInput.expected = initialize ? clazz.getComponentType() : int.class;
+            expressionInput.internal = true;
+            expression.analyze(scriptRoot, scope, expressionInput);
             expression.cast();
         }
 
-        actual = clazz;
+        output.actual = clazz;
+
+        return output;
     }
 
     @Override
@@ -78,7 +84,7 @@ public final class ENewArray extends AExpression {
         }
 
         newArrayNode.setLocation(location);
-        newArrayNode.setExpressionType(actual);
+        newArrayNode.setExpressionType(output.actual);
         newArrayNode.setInitialize(initialize);
 
         return newArrayNode;

+ 11 - 6
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewArrayFunctionRef.java

@@ -48,7 +48,10 @@ public final class ENewArrayFunctionRef extends AExpression implements ILambda {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
         SReturn code = new SReturn(location, new ENewArray(location, type, Arrays.asList(new EVariable(location, "size")), false));
         function = new SFunction(
                 location, type, scriptRoot.getNextSyntheticName("newarray"),
@@ -58,16 +61,18 @@ public final class ENewArrayFunctionRef extends AExpression implements ILambda {
         function.analyze(scriptRoot);
         scriptRoot.getFunctionTable().addFunction(function.name, function.returnType, function.typeParameters, true, true);
 
-        if (expected == null) {
+        if (input.expected == null) {
             ref = null;
-            actual = String.class;
+            output.actual = String.class;
             defPointer = "Sthis." + function.name + ",0";
         } else {
             defPointer = null;
             ref = FunctionRef.create(scriptRoot.getPainlessLookup(), scriptRoot.getFunctionTable(),
-                    location, expected, "this", function.name, 0);
-            actual = expected;
+                    location, input.expected, "this", function.name, 0);
+            output.actual = input.expected;
         }
+
+        return output;
     }
 
     @Override
@@ -77,7 +82,7 @@ public final class ENewArrayFunctionRef extends AExpression implements ILambda {
         NewArrayFuncRefNode newArrayFuncRefNode = new NewArrayFuncRefNode();
 
         newArrayFuncRefNode.setLocation(location);
-        newArrayFuncRefNode.setExpressionType(actual);
+        newArrayFuncRefNode.setExpressionType(output.actual);
         newArrayFuncRefNode.setFuncRef(ref);
 
         return newArrayFuncRefNode;

+ 18 - 12
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewObj.java

@@ -51,18 +51,21 @@ public final class ENewObj extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        actual = scriptRoot.getPainlessLookup().canonicalTypeNameToType(this.type);
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
 
-        if (actual == null) {
+        output.actual = scriptRoot.getPainlessLookup().canonicalTypeNameToType(this.type);
+
+        if (output.actual == null) {
             throw createError(new IllegalArgumentException("Not a type [" + this.type + "]."));
         }
 
-        constructor = scriptRoot.getPainlessLookup().lookupPainlessConstructor(actual, arguments.size());
+        constructor = scriptRoot.getPainlessLookup().lookupPainlessConstructor(output.actual, arguments.size());
 
         if (constructor == null) {
             throw createError(new IllegalArgumentException(
-                    "constructor [" + typeToCanonicalTypeName(actual) + ", <init>/" + arguments.size() + "] not found"));
+                    "constructor [" + typeToCanonicalTypeName(output.actual) + ", <init>/" + arguments.size() + "] not found"));
         }
 
         scriptRoot.markNonDeterministic(constructor.annotations.containsKey(NonDeterministicAnnotation.class));
@@ -72,20 +75,23 @@ public final class ENewObj extends AExpression {
 
         if (constructor.typeParameters.size() != arguments.size()) {
             throw createError(new IllegalArgumentException(
-                    "When calling constructor on type [" + PainlessLookupUtility.typeToCanonicalTypeName(actual) + "] " +
+                    "When calling constructor on type [" + PainlessLookupUtility.typeToCanonicalTypeName(output.actual) + "] " +
                     "expected [" + constructor.typeParameters.size() + "] arguments, but found [" + arguments.size() + "]."));
         }
 
         for (int argument = 0; argument < arguments.size(); ++argument) {
             AExpression expression = arguments.get(argument);
 
-            expression.expected = types[argument];
-            expression.internal = true;
-            expression.analyze(scriptRoot, scope);
+            Input expressionInput = new Input();
+            expressionInput.expected = types[argument];
+            expressionInput.internal = true;
+            expression.analyze(scriptRoot, scope, expressionInput);
             expression.cast();
         }
 
-        statement = true;
+        output.statement = true;
+
+        return output;
     }
 
     @Override
@@ -97,8 +103,8 @@ public final class ENewObj extends AExpression {
         }
 
         newObjectNode.setLocation(location);
-        newObjectNode.setExpressionType(actual);
-        newObjectNode.setRead(read);
+        newObjectNode.setExpressionType(output.actual);
+        newObjectNode.setRead(input.read);
         newObjectNode.setConstructor(constructor);
 
         return newObjectNode;

+ 13 - 8
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENull.java

@@ -36,21 +36,26 @@ public final class ENull extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        if (!read) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
+        if (input.read == false) {
             throw createError(new IllegalArgumentException("Must read from null constant."));
         }
 
-        if (expected != null) {
-            if (expected.isPrimitive()) {
+        if (input.expected != null) {
+            if (input.expected.isPrimitive()) {
                 throw createError(new IllegalArgumentException(
-                    "Cannot cast null to a primitive type [" + PainlessLookupUtility.typeToCanonicalTypeName(expected) + "]."));
+                    "Cannot cast null to a primitive type [" + PainlessLookupUtility.typeToCanonicalTypeName(input.expected) + "]."));
             }
 
-            actual = expected;
+            output.actual = input.expected;
         } else {
-            actual = Object.class;
+            output.actual = Object.class;
         }
+
+        return output;
     }
 
     @Override
@@ -58,7 +63,7 @@ public final class ENull extends AExpression {
         NullNode nullNode = new NullNode();
 
         nullNode.setLocation(location);
-        nullNode.setExpressionType(actual);
+        nullNode.setExpressionType(output.actual);
 
         return nullNode;
     }

+ 16 - 11
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENumeric.java

@@ -46,8 +46,11 @@ public final class ENumeric extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        if (!read) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
+        if (input.read == false) {
             throw createError(new IllegalArgumentException("Must read from constant [" + value + "]."));
         }
 
@@ -58,7 +61,7 @@ public final class ENumeric extends AExpression {
 
             try {
                 constant = Double.parseDouble(value.substring(0, value.length() - 1));
-                actual = double.class;
+                output.actual = double.class;
             } catch (NumberFormatException exception) {
                 throw createError(new IllegalArgumentException("Invalid double constant [" + value + "]."));
             }
@@ -69,34 +72,34 @@ public final class ENumeric extends AExpression {
 
             try {
                 constant = Float.parseFloat(value.substring(0, value.length() - 1));
-                actual = float.class;
+                output.actual = float.class;
             } catch (NumberFormatException exception) {
                 throw createError(new IllegalArgumentException("Invalid float constant [" + value + "]."));
             }
         } else if (value.endsWith("l") || value.endsWith("L")) {
             try {
                 constant = Long.parseLong(value.substring(0, value.length() - 1), radix);
-                actual = long.class;
+                output.actual = long.class;
             } catch (NumberFormatException exception) {
                 throw createError(new IllegalArgumentException("Invalid long constant [" + value + "]."));
             }
         } else {
             try {
-                Class<?> sort = expected == null ? int.class : expected;
+                Class<?> sort = input.expected == null ? int.class : input.expected;
                 int integer = Integer.parseInt(value, radix);
 
                 if (sort == byte.class && integer >= Byte.MIN_VALUE && integer <= Byte.MAX_VALUE) {
                     constant = (byte)integer;
-                    actual = byte.class;
+                    output.actual = byte.class;
                 } else if (sort == char.class && integer >= Character.MIN_VALUE && integer <= Character.MAX_VALUE) {
                     constant = (char)integer;
-                    actual = char.class;
+                    output.actual = char.class;
                 } else if (sort == short.class && integer >= Short.MIN_VALUE && integer <= Short.MAX_VALUE) {
                     constant = (short)integer;
-                    actual = short.class;
+                    output.actual = short.class;
                 } else {
                     constant = integer;
-                    actual = int.class;
+                    output.actual = int.class;
                 }
             } catch (NumberFormatException exception) {
                 try {
@@ -110,13 +113,15 @@ public final class ENumeric extends AExpression {
                 throw createError(new IllegalArgumentException("Invalid int constant [" + value + "]."));
             }
         }
+
+        return output;
     }
 
     @Override
     ExpressionNode write(ClassNode classNode) {
         ConstantNode constantNode = new ConstantNode();
         constantNode.setLocation(location);
-        constantNode.setExpressionType(actual);
+        constantNode.setExpressionType(output.actual);
         constantNode.setConstant(constant);
 
         return constantNode;

+ 9 - 3
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ERegex.java

@@ -63,14 +63,18 @@ public final class ERegex extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
+
         if (scriptRoot.getCompilerSettings().areRegexesEnabled() == false) {
             throw createError(new IllegalStateException("Regexes are disabled. Set [script.painless.regex.enabled] to [true] "
                     + "in elasticsearch.yaml to allow them. Be careful though, regexes break out of Painless's protection against deep "
                     + "recursion and long loops."));
         }
 
-        if (!read) {
+        if (input.read == false) {
             throw createError(new IllegalArgumentException("Regex constant may only be read [" + pattern + "]."));
         }
 
@@ -82,7 +86,9 @@ public final class ERegex extends AExpression {
         }
 
         name = scriptRoot.getNextSyntheticName("regex");
-        actual = Pattern.class;
+        output.actual = Pattern.class;
+
+        return output;
     }
 
     @Override

+ 9 - 4
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EStatic.java

@@ -41,12 +41,17 @@ public final class EStatic extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        actual = scriptRoot.getPainlessLookup().canonicalTypeNameToType(type);
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
 
-        if (actual == null) {
+        output.actual = scriptRoot.getPainlessLookup().canonicalTypeNameToType(type);
+
+        if (output.actual == null) {
             throw createError(new IllegalArgumentException("Not a type [" + type + "]."));
         }
+
+        return output;
     }
 
     @Override
@@ -54,7 +59,7 @@ public final class EStatic extends AExpression {
         StaticNode staticNode = new StaticNode();
 
         staticNode.setLocation(location);
-        staticNode.setExpressionType(actual);
+        staticNode.setExpressionType(output.actual);
 
         return staticNode;
     }

+ 9 - 4
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EString.java

@@ -42,19 +42,24 @@ public final class EString extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        if (!read) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
+        if (input.read == false) {
             throw createError(new IllegalArgumentException("Must read from constant [" + constant + "]."));
         }
 
-        actual = String.class;
+        output.actual = String.class;
+
+        return output;
     }
 
     @Override
     ExpressionNode write(ClassNode classNode) {
         ConstantNode constantNode = new ConstantNode();
         constantNode.setLocation(location);
-        constantNode.setExpressionType(actual);
+        constantNode.setExpressionType(output.actual);
         constantNode.setConstant(constant);
 
         return constantNode;

+ 33 - 78
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EUnary.java

@@ -51,88 +51,43 @@ public final class EUnary extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        originallyExplicit = explicit;
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
 
-        if (operation == Operation.NOT) {
-            analyzeNot(scriptRoot, scope);
-        } else if (operation == Operation.BWNOT) {
-            analyzeBWNot(scriptRoot, scope);
-        } else if (operation == Operation.ADD) {
-            analyzerAdd(scriptRoot, scope);
-        } else if (operation == Operation.SUB) {
-            analyzerSub(scriptRoot, scope);
-        } else {
-            throw createError(new IllegalStateException("Illegal tree structure."));
-        }
-    }
-
-    void analyzeNot(ScriptRoot scriptRoot, Scope variables) {
-        child.expected = boolean.class;
-        child.analyze(scriptRoot, variables);
-        child.cast();
-
-        actual = boolean.class;
-    }
-
-    void analyzeBWNot(ScriptRoot scriptRoot, Scope variables) {
-        child.analyze(scriptRoot, variables);
-
-        promote = AnalyzerCaster.promoteNumeric(child.actual, false);
-
-        if (promote == null) {
-            throw createError(new ClassCastException("Cannot apply not [~] to type " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(child.actual) + "]."));
-        }
-
-        child.expected = promote;
-        child.cast();
-
-        if (promote == def.class && expected != null) {
-            actual = expected;
-        } else {
-            actual = promote;
-        }
-    }
-
-    void analyzerAdd(ScriptRoot scriptRoot, Scope variables) {
-        child.analyze(scriptRoot, variables);
-
-        promote = AnalyzerCaster.promoteNumeric(child.actual, true);
-
-        if (promote == null) {
-            throw createError(new ClassCastException("Cannot apply positive [+] to type " +
-                    "[" + PainlessLookupUtility.typeToJavaType(child.actual) + "]."));
-        }
-
-        child.expected = promote;
-        child.cast();
+        originallyExplicit = input.explicit;
 
-        if (promote == def.class && expected != null) {
-            actual = expected;
+        if (operation == Operation.NOT) {
+            Input childInput = new Input();
+            childInput.expected = boolean.class;
+            child.analyze(scriptRoot, scope, childInput);
+            child.cast();
+
+            output.actual = boolean.class;
+        } else if (operation == Operation.BWNOT || operation == Operation.ADD || operation == Operation.SUB) {
+            Output childOutput = child.analyze(scriptRoot, scope, new Input());
+
+            promote = AnalyzerCaster.promoteNumeric(childOutput.actual, operation != Operation.BWNOT);
+
+            if (promote == null) {
+                throw createError(new ClassCastException("cannot apply the " + operation.name + " operator " +
+                        "[" + operation.symbol + "] to the type " +
+                        "[" + PainlessLookupUtility.typeToCanonicalTypeName(childOutput.actual) + "]"));
+            }
+
+            child.input.expected = promote;
+            child.cast();
+
+            if (promote == def.class && input.expected != null) {
+                output.actual = input.expected;
+            } else {
+                output.actual = promote;
+            }
         } else {
-            actual = promote;
+            throw createError(new IllegalStateException("unexpected unary operation [" + operation.name + "]"));
         }
-    }
 
-    void analyzerSub(ScriptRoot scriptRoot, Scope variables) {
-        child.analyze(scriptRoot, variables);
-
-        promote = AnalyzerCaster.promoteNumeric(child.actual, true);
-
-        if (promote == null) {
-            throw createError(new ClassCastException("Cannot apply negative [-] to type " +
-                    "[" + PainlessLookupUtility.typeToJavaType(child.actual) + "]."));
-        }
-
-        child.expected = promote;
-        child.cast();
-
-        if (promote == def.class && expected != null) {
-            actual = expected;
-        } else {
-            actual = promote;
-        }
+        return output;
     }
 
     @Override
@@ -142,7 +97,7 @@ public final class EUnary extends AExpression {
         unaryMathNode.setChildNode(child.cast(child.write(classNode)));
 
         unaryMathNode.setLocation(location);
-        unaryMathNode.setExpressionType(actual);
+        unaryMathNode.setExpressionType(output.actual);
         unaryMathNode.setUnaryType(promote);
         unaryMathNode.setOperation(operation);
         unaryMathNode.setOriginallExplicit(originallyExplicit);

+ 20 - 4
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/EVariable.java

@@ -42,14 +42,30 @@ public final class EVariable extends AStoreable {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, AExpression.Input input) {
+        AStoreable.Input storeableInput = new AStoreable.Input();
+        storeableInput.read = input.read;
+        storeableInput.expected = input.expected;
+        storeableInput.explicit = input.explicit;
+        storeableInput.internal = input.internal;
+
+        return analyze(scriptRoot, scope, storeableInput);
+    }
+
+    @Override
+    Output analyze(ScriptRoot scriptRoot, Scope scope, AStoreable.Input input) {
+        this.input = input;
+        output = new Output();
+
         Variable variable = scope.getVariable(location, name);
 
-        if (write && variable.isFinal()) {
+        if (input.write && variable.isFinal()) {
             throw createError(new IllegalArgumentException("Variable [" + variable.getName() + "] is read-only."));
         }
 
-        actual = variable.getType();
+        output.actual = variable.getType();
+
+        return output;
     }
 
     @Override
@@ -57,7 +73,7 @@ public final class EVariable extends AStoreable {
         VariableNode variableNode = new VariableNode();
 
         variableNode.setLocation(location);
-        variableNode.setExpressionType(actual);
+        variableNode.setExpressionType(output.actual);
         variableNode.setName(name);
 
         return variableNode;

+ 36 - 19
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PBrace.java

@@ -47,30 +47,47 @@ public final class PBrace extends AStoreable {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        prefix.analyze(scriptRoot, scope);
-        prefix.expected = prefix.actual;
+    Output analyze(ScriptRoot scriptRoot, Scope scope, AExpression.Input input) {
+        AStoreable.Input storeableInput = new AStoreable.Input();
+        storeableInput.read = input.read;
+        storeableInput.expected = input.expected;
+        storeableInput.explicit = input.explicit;
+        storeableInput.internal = input.internal;
+
+        return analyze(scriptRoot, scope, storeableInput);
+    }
+
+    @Override
+    Output analyze(ScriptRoot scriptRoot, Scope scope, AStoreable.Input input) {
+        this.input = input;
+        output = new Output();
+
+        Output prefixOutput = prefix.analyze(scriptRoot, scope, new Input());
+        prefix.input.expected = prefixOutput.actual;
         prefix.cast();
 
-        if (prefix.actual.isArray()) {
-            sub = new PSubBrace(location, prefix.actual, index);
-        } else if (prefix.actual == def.class) {
+        if (prefixOutput.actual.isArray()) {
+            sub = new PSubBrace(location, prefixOutput.actual, index);
+        } else if (prefixOutput.actual == def.class) {
             sub = new PSubDefArray(location, index);
-        } else if (Map.class.isAssignableFrom(prefix.actual)) {
-            sub = new PSubMapShortcut(location, prefix.actual, index);
-        } else if (List.class.isAssignableFrom(prefix.actual)) {
-            sub = new PSubListShortcut(location, prefix.actual, index);
+        } else if (Map.class.isAssignableFrom(prefixOutput.actual)) {
+            sub = new PSubMapShortcut(location, prefixOutput.actual, index);
+        } else if (List.class.isAssignableFrom(prefixOutput.actual)) {
+            sub = new PSubListShortcut(location, prefixOutput.actual, index);
         } else {
             throw createError(new IllegalArgumentException("Illegal array access on type " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(prefix.actual) + "]."));
+                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(prefixOutput.actual) + "]."));
         }
 
-        sub.write = write;
-        sub.read = read;
-        sub.expected = expected;
-        sub.explicit = explicit;
-        sub.analyze(scriptRoot, scope);
-        actual = sub.actual;
+        Input subInput = new Input();
+        subInput.write = input.write;
+        subInput.read = input.read;
+        subInput.expected = input.expected;
+        subInput.explicit = input.explicit;
+        Output subOutput = sub.analyze(scriptRoot, scope, subInput);
+        output.actual = subOutput.actual;
+
+        return output;
     }
 
     @Override
@@ -81,7 +98,7 @@ public final class PBrace extends AStoreable {
         braceNode.setRightNode(sub.write(classNode));
 
         braceNode.setLocation(location);
-        braceNode.setExpressionType(actual);
+        braceNode.setExpressionType(output.actual);
 
         return braceNode;
     }
@@ -94,7 +111,7 @@ public final class PBrace extends AStoreable {
     @Override
     void updateActual(Class<?> actual) {
         sub.updateActual(actual);
-        this.actual = actual;
+        this.output.actual = actual;
     }
 
     @Override

+ 20 - 14
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PCallInvoke.java

@@ -53,37 +53,43 @@ public final class PCallInvoke extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        prefix.analyze(scriptRoot, scope);
-        prefix.expected = prefix.actual;
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
+        Output prefixOutput = prefix.analyze(scriptRoot, scope, new Input());
+        prefix.input.expected = prefixOutput.actual;
         prefix.cast();
 
-        if (prefix.actual == def.class) {
+        if (prefixOutput.actual == def.class) {
             sub = new PSubDefCall(location, name, arguments);
         } else {
-            PainlessMethod method =
-                    scriptRoot.getPainlessLookup().lookupPainlessMethod(prefix.actual, prefix instanceof EStatic, name, arguments.size());
+            PainlessMethod method = scriptRoot.getPainlessLookup().lookupPainlessMethod(
+                    prefixOutput.actual, prefix instanceof EStatic, name, arguments.size());
 
             if (method == null) {
                 throw createError(new IllegalArgumentException(
-                        "method [" + typeToCanonicalTypeName(prefix.actual) + ", " + name + "/" + arguments.size() + "] not found"));
+                        "method [" + typeToCanonicalTypeName(prefixOutput.actual) + ", " + name + "/" + arguments.size() + "] not found"));
             }
 
             scriptRoot.markNonDeterministic(method.annotations.containsKey(NonDeterministicAnnotation.class));
 
-            sub = new PSubCallInvoke(location, method, prefix.actual, arguments);
+            sub = new PSubCallInvoke(location, method, prefixOutput.actual, arguments);
         }
 
         if (nullSafe) {
             sub = new PSubNullSafeCallInvoke(location, sub);
         }
 
-        sub.expected = expected;
-        sub.explicit = explicit;
-        sub.analyze(scriptRoot, scope);
-        actual = sub.actual;
+        Input subInput = new Input();
+        subInput.expected = input.expected;
+        subInput.explicit = input.explicit;
+        Output subOutput = sub.analyze(scriptRoot, scope, subInput);
+        output.actual = subOutput.actual;
+
+        output.statement = true;
 
-        statement = true;
+        return output;
     }
 
     @Override
@@ -94,7 +100,7 @@ public final class PCallInvoke extends AExpression {
         callNode.setRightNode(sub.write(classNode));
 
         callNode.setLocation(location);
-        callNode.setExpressionType(actual);
+        callNode.setExpressionType(output.actual);
 
         return callNode;
     }

+ 43 - 25
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PField.java

@@ -53,51 +53,66 @@ public final class PField extends AStoreable {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        prefix.analyze(scriptRoot, scope);
-        prefix.expected = prefix.actual;
+    Output analyze(ScriptRoot scriptRoot, Scope scope, AExpression.Input input) {
+        AStoreable.Input storeableInput = new AStoreable.Input();
+        storeableInput.read = input.read;
+        storeableInput.expected = input.expected;
+        storeableInput.explicit = input.explicit;
+        storeableInput.internal = input.internal;
+
+        return analyze(scriptRoot, scope, storeableInput);
+    }
+
+    @Override
+    Output analyze(ScriptRoot scriptRoot, Scope scope, AStoreable.Input input) {
+        this.input = input;
+        output = new Output();
+
+        Output prefixOutput = prefix.analyze(scriptRoot, scope, new Input());
+        prefix.input.expected = prefixOutput.actual;
         prefix.cast();
 
-        if (prefix.actual.isArray()) {
-            sub = new PSubArrayLength(location, PainlessLookupUtility.typeToCanonicalTypeName(prefix.actual), value);
-        } else if (prefix.actual == def.class) {
+        if (prefixOutput.actual.isArray()) {
+            sub = new PSubArrayLength(location, PainlessLookupUtility.typeToCanonicalTypeName(prefixOutput.actual), value);
+        } else if (prefixOutput.actual == def.class) {
             sub = new PSubDefField(location, value);
         } else {
-            PainlessField field = scriptRoot.getPainlessLookup().lookupPainlessField(prefix.actual, prefix instanceof EStatic, value);
+            PainlessField field = scriptRoot.getPainlessLookup().lookupPainlessField(prefixOutput.actual, prefix instanceof EStatic, value);
 
             if (field == null) {
                 PainlessMethod getter;
                 PainlessMethod setter;
 
-                getter = scriptRoot.getPainlessLookup().lookupPainlessMethod(prefix.actual, false,
+                getter = scriptRoot.getPainlessLookup().lookupPainlessMethod(prefixOutput.actual, false,
                         "get" + Character.toUpperCase(value.charAt(0)) + value.substring(1), 0);
 
                 if (getter == null) {
-                    getter = scriptRoot.getPainlessLookup().lookupPainlessMethod(prefix.actual, false,
+                    getter = scriptRoot.getPainlessLookup().lookupPainlessMethod(prefixOutput.actual, false,
                             "is" + Character.toUpperCase(value.charAt(0)) + value.substring(1), 0);
                 }
 
-                setter = scriptRoot.getPainlessLookup().lookupPainlessMethod(prefix.actual, false,
+                setter = scriptRoot.getPainlessLookup().lookupPainlessMethod(prefixOutput.actual, false,
                         "set" + Character.toUpperCase(value.charAt(0)) + value.substring(1), 0);
 
                 if (getter != null || setter != null) {
-                    sub = new PSubShortcut(location, value, PainlessLookupUtility.typeToCanonicalTypeName(prefix.actual), getter, setter);
+                    sub = new PSubShortcut(
+                            location, value, PainlessLookupUtility.typeToCanonicalTypeName(prefixOutput.actual), getter, setter);
                 } else {
                     EConstant index = new EConstant(location, value);
-                    index.analyze(scriptRoot, scope);
+                    index.analyze(scriptRoot, scope, new Input());
 
-                    if (Map.class.isAssignableFrom(prefix.actual)) {
-                        sub = new PSubMapShortcut(location, prefix.actual, index);
+                    if (Map.class.isAssignableFrom(prefixOutput.actual)) {
+                        sub = new PSubMapShortcut(location, prefixOutput.actual, index);
                     }
 
-                    if (List.class.isAssignableFrom(prefix.actual)) {
-                        sub = new PSubListShortcut(location, prefix.actual, index);
+                    if (List.class.isAssignableFrom(prefixOutput.actual)) {
+                        sub = new PSubListShortcut(location, prefixOutput.actual, index);
                     }
                 }
 
                 if (sub == null) {
                     throw createError(new IllegalArgumentException(
-                            "field [" + typeToCanonicalTypeName(prefix.actual) + ", " + value + "] not found"));
+                            "field [" + typeToCanonicalTypeName(prefixOutput.actual) + ", " + value + "] not found"));
                 }
             } else {
                 sub = new PSubField(location, field);
@@ -108,12 +123,15 @@ public final class PField extends AStoreable {
             sub = new PSubNullSafeField(location, sub);
         }
 
-        sub.write = write;
-        sub.read = read;
-        sub.expected = expected;
-        sub.explicit = explicit;
-        sub.analyze(scriptRoot, scope);
-        actual = sub.actual;
+        Input subInput = new Input();
+        subInput.write = input.write;
+        subInput.read = input.read;
+        subInput.expected = input.expected;
+        subInput.explicit = input.explicit;
+        Output subOutput = sub.analyze(scriptRoot, scope, subInput);
+        output.actual = subOutput.actual;
+
+        return output;
     }
 
     @Override
@@ -124,7 +142,7 @@ public final class PField extends AStoreable {
         dotNode.setRightNode(sub.write(classNode));
 
         dotNode.setLocation(location);
-        dotNode.setExpressionType(actual);
+        dotNode.setExpressionType(output.actual);
 
         return dotNode;
     }
@@ -137,7 +155,7 @@ public final class PField extends AStoreable {
     @Override
     void updateActual(Class<?> actual) {
         sub.updateActual(actual);
-        this.actual = actual;
+        this.output.actual = actual;
     }
 
     @Override

+ 9 - 4
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubArrayLength.java

@@ -43,16 +43,21 @@ final class PSubArrayLength extends AStoreable {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, AStoreable.Input input) {
+        this.input = input;
+        output = new Output();
+
         if ("length".equals(value)) {
-            if (write) {
+            if (input.write) {
                 throw createError(new IllegalArgumentException("Cannot write to read-only field [length] for an array."));
             }
 
-            actual = int.class;
+            output.actual = int.class;
         } else {
             throw createError(new IllegalArgumentException("Field [" + value + "] does not exist for type [" + type + "]."));
         }
+
+        return output;
     }
 
     @Override
@@ -60,7 +65,7 @@ final class PSubArrayLength extends AStoreable {
         DotSubArrayLengthNode dotSubArrayLengthNode = new DotSubArrayLengthNode();
 
         dotSubArrayLengthNode.setLocation(location);
-        dotSubArrayLengthNode.setExpressionType(actual);
+        dotSubArrayLengthNode.setExpressionType(output.actual);
 
         return dotSubArrayLengthNode;
     }

+ 11 - 5
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubBrace.java

@@ -43,12 +43,18 @@ final class PSubBrace extends AStoreable {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        index.expected = int.class;
-        index.analyze(scriptRoot, scope);
+    Output analyze(ScriptRoot scriptRoot, Scope scope, AStoreable.Input input) {
+        this.input = input;
+        output = new Output();
+
+        Input indexInput = new Input();
+        indexInput.expected = int.class;
+        index.analyze(scriptRoot, scope, indexInput);
         index.cast();
 
-        actual = clazz.getComponentType();
+        output.actual = clazz.getComponentType();
+
+        return output;
     }
 
     BraceSubNode write(ClassNode classNode) {
@@ -57,7 +63,7 @@ final class PSubBrace extends AStoreable {
         braceSubNode.setChildNode(index.cast(index.write(classNode)));
 
         braceSubNode.setLocation(location);
-        braceSubNode.setExpressionType(actual);
+        braceSubNode.setExpressionType(output.actual);
 
         return braceSubNode;
     }

+ 13 - 7
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubCallInvoke.java

@@ -47,18 +47,24 @@ final class PSubCallInvoke extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
         for (int argument = 0; argument < arguments.size(); ++argument) {
             AExpression expression = arguments.get(argument);
 
-            expression.expected = method.typeParameters.get(argument);
-            expression.internal = true;
-            expression.analyze(scriptRoot, scope);
+            Input expressionInput = new Input();
+            expressionInput.expected = method.typeParameters.get(argument);
+            expressionInput.internal = true;
+            expression.analyze(scriptRoot, scope, expressionInput);
             expression.cast();
         }
 
-        statement = true;
-        actual = method.returnType;
+        output.statement = true;
+        output.actual = method.returnType;
+
+        return output;
     }
 
     @Override
@@ -70,7 +76,7 @@ final class PSubCallInvoke extends AExpression {
         }
 
         callSubNode.setLocation(location);
-        callSubNode.setExpressionType(actual);
+        callSubNode.setExpressionType(output.actual);
         callSubNode.setMethod(method);
         callSubNode .setBox(box);
 

+ 11 - 6
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefArray.java

@@ -42,13 +42,18 @@ final class PSubDefArray extends AStoreable {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        index.analyze(scriptRoot, scope);
-        index.expected = index.actual;
+    Output analyze(ScriptRoot scriptRoot, Scope scope, AStoreable.Input input) {
+        this.input = input;
+        output = new Output();
+
+        Output indexOutput = index.analyze(scriptRoot, scope, new Input());
+        index.input.expected = indexOutput.actual;
         index.cast();
 
         // TODO: remove ZonedDateTime exception when JodaCompatibleDateTime is removed
-        actual = expected == null || expected == ZonedDateTime.class || explicit ? def.class : expected;
+        output.actual = input.expected == null || input.expected == ZonedDateTime.class || input.explicit ? def.class : input.expected;
+
+        return output;
     }
 
     @Override
@@ -58,7 +63,7 @@ final class PSubDefArray extends AStoreable {
         braceSubDefNode.setChildNode(index.cast(index.write(classNode)));
 
         braceSubDefNode.setLocation(location);
-        braceSubDefNode.setExpressionType(actual);
+        braceSubDefNode.setExpressionType(output.actual);
 
         return braceSubDefNode;
     }
@@ -70,7 +75,7 @@ final class PSubDefArray extends AStoreable {
 
     @Override
     void updateActual(Class<?> actual) {
-        this.actual = actual;
+        this.output.actual = actual;
     }
 
     @Override

+ 14 - 8
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefCall.java

@@ -51,23 +51,27 @@ final class PSubDefCall extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
         parameterTypes.add(Object.class);
         int totalCaptures = 0;
 
         for (int argument = 0; argument < arguments.size(); ++argument) {
             AExpression expression = arguments.get(argument);
 
-            expression.internal = true;
-            expression.analyze(scriptRoot, scope);
+            Input expressionInput = new Input();
+            expressionInput.internal = true;
+            Output expressionOutput = expression.analyze(scriptRoot, scope, expressionInput);
 
-            if (expression.actual == void.class) {
+            if (expressionOutput.actual == void.class) {
                 throw createError(new IllegalArgumentException("Argument(s) cannot be of [void] type when calling method [" + name + "]."));
             }
 
-            expression.expected = expression.actual;
+            expression.input.expected = expressionOutput.actual;
             expression.cast();
-            parameterTypes.add(expression.actual);
+            parameterTypes.add(expressionOutput.actual);
 
             if (expression instanceof ILambda) {
                 ILambda lambda = (ILambda) expression;
@@ -81,7 +85,9 @@ final class PSubDefCall extends AExpression {
         }
 
         // TODO: remove ZonedDateTime exception when JodaCompatibleDateTime is removed
-        actual = expected == null || expected == ZonedDateTime.class || explicit ? def.class : expected;
+        output.actual = input.expected == null || input.expected == ZonedDateTime.class || input.explicit ? def.class : input.expected;
+
+        return output;
     }
 
     @Override
@@ -93,7 +99,7 @@ final class PSubDefCall extends AExpression {
         }
 
         callSubDefNode.setLocation(location);
-        callSubDefNode.setExpressionType(actual);
+        callSubDefNode.setExpressionType(output.actual);
         callSubDefNode.setName(name);
         callSubDefNode.setRecipe(recipe.toString());
         callSubDefNode.getPointers().addAll(pointers);

+ 9 - 4
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubDefField.java

@@ -43,9 +43,14 @@ final class PSubDefField extends AStoreable {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, AStoreable.Input input) {
+        this.input = input;
+        output = new Output();
+
         // TODO: remove ZonedDateTime exception when JodaCompatibleDateTime is removed
-        actual = expected == null || expected == ZonedDateTime.class || explicit ? def.class : expected;
+        output.actual = input.expected == null || input.expected == ZonedDateTime.class || input.explicit ? def.class : input.expected;
+
+        return output;
     }
 
     @Override
@@ -53,7 +58,7 @@ final class PSubDefField extends AStoreable {
         DotSubDefNode dotSubDefNode = new DotSubDefNode();
 
         dotSubDefNode.setLocation(location);
-        dotSubDefNode.setExpressionType(actual);
+        dotSubDefNode.setExpressionType(output.actual);
         dotSubDefNode.setValue(value);
 
         return dotSubDefNode;
@@ -66,7 +71,7 @@ final class PSubDefField extends AStoreable {
 
     @Override
     void updateActual(Class<?> actual) {
-        this.actual = actual;
+        this.output.actual = actual;
     }
 
     @Override

+ 9 - 4
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubField.java

@@ -44,13 +44,18 @@ final class PSubField extends AStoreable {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-         if (write && Modifier.isFinal(field.javaField.getModifiers())) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, AStoreable.Input input) {
+        this.input = input;
+        output = new Output();
+
+         if (input.write && Modifier.isFinal(field.javaField.getModifiers())) {
              throw createError(new IllegalArgumentException("Cannot write to read-only field [" + field.javaField.getName() + "] " +
                      "for type [" + PainlessLookupUtility.typeToCanonicalTypeName(field.javaField.getDeclaringClass()) + "]."));
          }
 
-        actual = field.typeParameter;
+         output.actual = field.typeParameter;
+
+         return output;
     }
 
     @Override
@@ -58,7 +63,7 @@ final class PSubField extends AStoreable {
         DotSubNode dotSubNode = new DotSubNode();
 
         dotSubNode.setLocation(location);
-        dotSubNode.setExpressionType(actual);
+        dotSubNode.setExpressionType(output.actual);
         dotSubNode.setField(field);
 
         return dotSubNode;

+ 12 - 6
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubListShortcut.java

@@ -48,7 +48,10 @@ final class PSubListShortcut extends AStoreable {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, AStoreable.Input input) {
+        this.input = input;
+        output = new Output();
+
         String canonicalClassName = PainlessLookupUtility.typeToCanonicalTypeName(targetClass);
 
         getter = scriptRoot.getPainlessLookup().lookupPainlessMethod(targetClass, false, "get", 1);
@@ -68,15 +71,18 @@ final class PSubListShortcut extends AStoreable {
             throw createError(new IllegalArgumentException("Shortcut argument types must match."));
         }
 
-        if ((read || write) && (!read || getter != null) && (!write || setter != null)) {
-            index.expected = int.class;
-            index.analyze(scriptRoot, scope);
+        if ((input.read || input.write) && (input.read == false || getter != null) && (input.write == false || setter != null)) {
+            Input indexInput = new Input();
+            indexInput.expected = int.class;
+            index.analyze(scriptRoot, scope, indexInput);
             index.cast();
 
-            actual = setter != null ? setter.typeParameters.get(1) : getter.returnType;
+            output.actual = setter != null ? setter.typeParameters.get(1) : getter.returnType;
         } else {
             throw createError(new IllegalArgumentException("Illegal list shortcut for type [" + canonicalClassName + "]."));
         }
+
+        return output;
     }
 
     @Override
@@ -86,7 +92,7 @@ final class PSubListShortcut extends AStoreable {
         listSubShortcutNode.setChildNode(index.cast(index.write(classNode)));
 
         listSubShortcutNode.setLocation(location);
-        listSubShortcutNode.setExpressionType(actual);
+        listSubShortcutNode.setExpressionType(output.actual);
         listSubShortcutNode.setGetter(getter);
         listSubShortcutNode.setSetter(setter);
 

+ 12 - 6
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubMapShortcut.java

@@ -48,7 +48,10 @@ final class PSubMapShortcut extends AStoreable {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, AStoreable.Input input) {
+        this.input = input;
+        output = new Output();
+
         String canonicalClassName = PainlessLookupUtility.typeToCanonicalTypeName(targetClass);
 
         getter = scriptRoot.getPainlessLookup().lookupPainlessMethod(targetClass, false, "get", 1);
@@ -67,15 +70,18 @@ final class PSubMapShortcut extends AStoreable {
             throw createError(new IllegalArgumentException("Shortcut argument types must match."));
         }
 
-        if ((read || write) && (!read || getter != null) && (!write || setter != null)) {
-            index.expected = setter != null ? setter.typeParameters.get(0) : getter.typeParameters.get(0);
-            index.analyze(scriptRoot, scope);
+        if ((input.read || input.write) && (input.read == false || getter != null) && (input.write == false || setter != null)) {
+            Input indexInput = new Input();
+            indexInput.expected = setter != null ? setter.typeParameters.get(0) : getter.typeParameters.get(0);
+            index.analyze(scriptRoot, scope, indexInput);
             index.cast();
 
-            actual = setter != null ? setter.typeParameters.get(1) : getter.returnType;
+            output.actual = setter != null ? setter.typeParameters.get(1) : getter.returnType;
         } else {
             throw createError(new IllegalArgumentException("Illegal map shortcut for type [" + canonicalClassName + "]."));
         }
+
+        return output;
     }
 
     @Override
@@ -85,7 +91,7 @@ final class PSubMapShortcut extends AStoreable {
         mapSubShortcutNode.setChildNode(index.cast(index.write(classNode)));
 
         mapSubShortcutNode.setLocation(location);
-        mapSubShortcutNode.setExpressionType(actual);
+        mapSubShortcutNode.setExpressionType(output.actual);
         mapSubShortcutNode.setGetter(getter);
         mapSubShortcutNode.setSetter(setter);
 

+ 10 - 5
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubNullSafeCallInvoke.java

@@ -42,12 +42,17 @@ public class PSubNullSafeCallInvoke extends AExpression {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        guarded.analyze(scriptRoot, scope);
-        actual = guarded.actual;
-        if (actual.isPrimitive()) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, Input input) {
+        this.input = input;
+        output = new Output();
+
+        Output guardedOutput = guarded.analyze(scriptRoot, scope, new Input());
+        output.actual = guardedOutput.actual;
+        if (output.actual.isPrimitive()) {
             throw new IllegalArgumentException("Result of null safe operator must be nullable");
         }
+
+        return output;
     }
 
     @Override
@@ -57,7 +62,7 @@ public class PSubNullSafeCallInvoke extends AExpression {
         nullSafeSubNode.setChildNode(guarded.write(classNode));
 
         nullSafeSubNode.setLocation(location);
-        nullSafeSubNode.setExpressionType(actual);
+        nullSafeSubNode.setExpressionType(output.actual);
 
         return nullSafeSubNode;
     }

+ 13 - 7
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubNullSafeField.java

@@ -37,16 +37,22 @@ public class PSubNullSafeField extends AStoreable {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
-        if (write) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, AStoreable.Input input) {
+        this.input = input;
+        output = new Output();
+
+        if (input.write) {
             throw createError(new IllegalArgumentException("Can't write to null safe reference"));
         }
-        guarded.read = read;
-        guarded.analyze(scriptRoot, scope);
-        actual = guarded.actual;
-        if (actual.isPrimitive()) {
+        Input guardedInput = new Input();
+        guardedInput.read = input.read;
+        Output guardedOutput = guarded.analyze(scriptRoot, scope, guardedInput);
+        output.actual = guardedOutput.actual;
+        if (output.actual.isPrimitive()) {
             throw new IllegalArgumentException("Result of null safe operator must be nullable");
         }
+
+        return output;
     }
 
     @Override
@@ -66,7 +72,7 @@ public class PSubNullSafeField extends AStoreable {
         nullSafeSubNode.setChildNode(guarded.write(classNode));
 
         nullSafeSubNode.setLocation(location);
-        nullSafeSubNode.setExpressionType(actual);
+        nullSafeSubNode.setExpressionType(output.actual);
 
         return nullSafeSubNode;
     }

+ 9 - 4
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/PSubShortcut.java

@@ -46,7 +46,10 @@ final class PSubShortcut extends AStoreable {
     }
 
     @Override
-    void analyze(ScriptRoot scriptRoot, Scope scope) {
+    Output analyze(ScriptRoot scriptRoot, Scope scope, AStoreable.Input input) {
+        this.input = input;
+        output = new Output();
+
         if (getter != null && (getter.returnType == void.class || !getter.typeParameters.isEmpty())) {
             throw createError(new IllegalArgumentException(
                 "Illegal get shortcut on field [" + value + "] for type [" + type + "]."));
@@ -61,11 +64,13 @@ final class PSubShortcut extends AStoreable {
             throw createError(new IllegalArgumentException("Shortcut argument types must match."));
         }
 
-        if ((getter != null || setter != null) && (!read || getter != null) && (!write || setter != null)) {
-            actual = setter != null ? setter.typeParameters.get(0) : getter.returnType;
+        if ((getter != null || setter != null) && (input.read == false || getter != null) && (input.write == false || setter != null)) {
+            output.actual = setter != null ? setter.typeParameters.get(0) : getter.returnType;
         } else {
             throw createError(new IllegalArgumentException("Illegal shortcut on field [" + value + "] for type [" + type + "]."));
         }
+
+        return output;
     }
 
     @Override
@@ -73,7 +78,7 @@ final class PSubShortcut extends AStoreable {
         DotSubShortcutNode dotSubShortcutNode = new DotSubShortcutNode();
 
         dotSubShortcutNode.setLocation(location);
-        dotSubShortcutNode.setExpressionType(actual);
+        dotSubShortcutNode.setExpressionType(output.actual);
         dotSubShortcutNode.setGetter(getter);
         dotSubShortcutNode.setSetter(setter);
 

+ 4 - 2
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDeclaration.java

@@ -23,6 +23,7 @@ import org.elasticsearch.painless.Location;
 import org.elasticsearch.painless.Scope;
 import org.elasticsearch.painless.ir.ClassNode;
 import org.elasticsearch.painless.ir.DeclarationNode;
+import org.elasticsearch.painless.node.AExpression.Input;
 import org.elasticsearch.painless.symbol.ScriptRoot;
 
 import java.util.Objects;
@@ -52,8 +53,9 @@ public final class SDeclaration extends AStatement {
         type = resolvedType;
 
         if (expression != null) {
-            expression.expected = resolvedType.getType();
-            expression.analyze(scriptRoot, scope);
+            Input expressionInput = new Input();
+            expressionInput.expected = resolvedType.getType();
+            expression.analyze(scriptRoot, scope, expressionInput);
             expression.cast();
         }
 

+ 3 - 2
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SDo.java

@@ -60,8 +60,9 @@ public final class SDo extends AStatement {
             throw createError(new IllegalArgumentException("Extraneous do while loop."));
         }
 
-        condition.expected = boolean.class;
-        condition.analyze(scriptRoot, scope);
+        AExpression.Input conditionInput = new AExpression.Input();
+        conditionInput.expected = boolean.class;
+        condition.analyze(scriptRoot, scope, conditionInput);
         condition.cast();
 
         if (condition instanceof EBoolean) {

+ 5 - 5
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SEach.java

@@ -54,8 +54,8 @@ public class SEach extends AStatement {
 
     @Override
     void analyze(ScriptRoot scriptRoot, Scope scope) {
-        expression.analyze(scriptRoot, scope);
-        expression.expected = expression.actual;
+        AExpression.Output expressionOutput = expression.analyze(scriptRoot, scope, new AExpression.Input());
+        expression.input.expected = expressionOutput.actual;
         expression.cast();
 
         Class<?> clazz = scriptRoot.getPainlessLookup().canonicalTypeNameToType(this.type);
@@ -67,13 +67,13 @@ public class SEach extends AStatement {
         scope = scope.newLocalScope();
         Variable variable = scope.defineVariable(location, clazz, name, true);
 
-        if (expression.actual.isArray()) {
+        if (expressionOutput.actual.isArray()) {
             sub = new SSubEachArray(location, variable, expression, block);
-        } else if (expression.actual == def.class || Iterable.class.isAssignableFrom(expression.actual)) {
+        } else if (expressionOutput.actual == def.class || Iterable.class.isAssignableFrom(expressionOutput.actual)) {
             sub = new SSubEachIterable(location, variable, expression, block);
         } else {
             throw createError(new IllegalArgumentException("Illegal for each type " +
-                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(expression.actual) + "]."));
+                    "[" + PainlessLookupUtility.typeToCanonicalTypeName(expressionOutput.actual) + "]."));
         }
 
         sub.analyze(scriptRoot, scope);

+ 8 - 6
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SExpression.java

@@ -48,17 +48,19 @@ public final class SExpression extends AStatement {
         Class<?> rtnType = scope.getReturnType();
         boolean isVoid = rtnType == void.class;
 
-        expression.read = lastSource && !isVoid;
-        expression.analyze(scriptRoot, scope);
+        AExpression.Input expressionInput = new AExpression.Input();
+        expressionInput.read = lastSource && !isVoid;
+        AExpression.Output expressionOutput = expression.analyze(scriptRoot, scope, expressionInput);
 
-        if ((lastSource == false || isVoid) && expression.statement == false) {
+
+        if ((lastSource == false || isVoid) && expressionOutput.statement == false) {
             throw createError(new IllegalArgumentException("Not a statement."));
         }
 
-        boolean rtn = lastSource && !isVoid && expression.actual != void.class;
+        boolean rtn = lastSource && isVoid == false && expressionOutput.actual != void.class;
 
-        expression.expected = rtn ? rtnType : expression.actual;
-        expression.internal = rtn;
+        expression.input.expected = rtn ? rtnType : expressionOutput.actual;
+        expression.input.internal = rtn;
         expression.cast();
 
         methodEscape = rtn;

+ 13 - 10
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SFor.java

@@ -61,14 +61,15 @@ public final class SFor extends AStatement {
             } else if (initializer instanceof AExpression) {
                 AExpression initializer = (AExpression)this.initializer;
 
-                initializer.read = false;
-                initializer.analyze(scriptRoot, scope);
+                AExpression.Input initializerInput = new AExpression.Input();
+                initializerInput.read = false;
+                AExpression.Output initializerOutput = initializer.analyze(scriptRoot, scope, initializerInput);
 
-                if (!initializer.statement) {
+                if (initializerOutput.statement == false) {
                     throw createError(new IllegalArgumentException("Not a statement."));
                 }
 
-                initializer.expected = initializer.actual;
+                initializer.input.expected = initializerOutput.actual;
                 initializer.cast();
             } else {
                 throw createError(new IllegalStateException("Illegal tree structure."));
@@ -76,8 +77,9 @@ public final class SFor extends AStatement {
         }
 
         if (condition != null) {
-            condition.expected = boolean.class;
-            condition.analyze(scriptRoot, scope);
+            AExpression.Input conditionInput = new AExpression.Input();
+            conditionInput.expected = boolean.class;
+            condition.analyze(scriptRoot, scope, conditionInput);
             condition.cast();
 
             if (condition instanceof EBoolean) {
@@ -96,14 +98,15 @@ public final class SFor extends AStatement {
         }
 
         if (afterthought != null) {
-            afterthought.read = false;
-            afterthought.analyze(scriptRoot, scope);
+            AExpression.Input afterthoughtInput = new AExpression.Input();
+            afterthoughtInput.read = false;
+            AExpression.Output afterthoughtOutput = afterthought.analyze(scriptRoot, scope, afterthoughtInput);
 
-            if (!afterthought.statement) {
+            if (afterthoughtOutput.statement == false) {
                 throw createError(new IllegalArgumentException("Not a statement."));
             }
 
-            afterthought.expected = afterthought.actual;
+            afterthought.input.expected = afterthoughtOutput.actual;
             afterthought.cast();
         }
 

+ 3 - 2
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SIf.java

@@ -44,8 +44,9 @@ public final class SIf extends AStatement {
 
     @Override
     void analyze(ScriptRoot scriptRoot, Scope scope) {
-        condition.expected = boolean.class;
-        condition.analyze(scriptRoot, scope);
+        AExpression.Input conditionInput = new AExpression.Input();
+        conditionInput.expected = boolean.class;
+        condition.analyze(scriptRoot, scope, conditionInput);
         condition.cast();
 
         if (condition instanceof EBoolean) {

+ 3 - 2
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SIfElse.java

@@ -49,8 +49,9 @@ public final class SIfElse extends AStatement {
 
     @Override
     void analyze(ScriptRoot scriptRoot, Scope scope) {
-        condition.expected = boolean.class;
-        condition.analyze(scriptRoot, scope);
+        AExpression.Input conditionInput = new AExpression.Input();
+        conditionInput.expected = boolean.class;
+        condition.analyze(scriptRoot, scope, conditionInput);
         condition.cast();
 
         if (condition instanceof EBoolean) {

+ 4 - 3
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SReturn.java

@@ -48,9 +48,10 @@ public final class SReturn extends AStatement {
                         "[" + PainlessLookupUtility.typeToCanonicalTypeName(void.class) + "]."));
             }
         } else {
-            expression.expected = scope.getReturnType();
-            expression.internal = true;
-            expression.analyze(scriptRoot, scope);
+            AExpression.Input expressionInput = new AExpression.Input();
+            expressionInput.expected = scope.getReturnType();
+            expressionInput.internal = true;
+            expression.analyze(scriptRoot, scope, expressionInput);
             expression.cast();
         }
 

+ 2 - 2
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSubEachArray.java

@@ -55,9 +55,9 @@ final class SSubEachArray extends AStatement {
     void analyze(ScriptRoot scriptRoot, Scope scope) {
         // We must store the array and index as variables for securing slots on the stack, and
         // also add the location offset to make the names unique in case of nested for each loops.
-        array = scope.defineInternalVariable(location, expression.actual, "array" + location.getOffset(), true);
+        array = scope.defineInternalVariable(location, expression.output.actual, "array" + location.getOffset(), true);
         index = scope.defineInternalVariable(location, int.class, "index" + location.getOffset(), true);
-        indexed = expression.actual.getComponentType();
+        indexed = expression.output.actual.getComponentType();
         cast = AnalyzerCaster.getLegalCast(location, indexed, variable.getType(), true, true);
     }
 

+ 3 - 3
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSubEachIterable.java

@@ -62,14 +62,14 @@ final class SSubEachIterable extends AStatement {
         // also add the location offset to make the name unique in case of nested for each loops.
         iterator = scope.defineInternalVariable(location, Iterator.class, "itr" + location.getOffset(), true);
 
-        if (expression.actual == def.class) {
+        if (expression.output.actual == def.class) {
             method = null;
         } else {
-            method = scriptRoot.getPainlessLookup().lookupPainlessMethod(expression.actual, false, "iterator", 0);
+            method = scriptRoot.getPainlessLookup().lookupPainlessMethod(expression.output.actual, false, "iterator", 0);
 
             if (method == null) {
                     throw createError(new IllegalArgumentException(
-                            "method [" + typeToCanonicalTypeName(expression.actual) + ", iterator/0] not found"));
+                            "method [" + typeToCanonicalTypeName(expression.output.actual) + ", iterator/0] not found"));
             }
         }
 

+ 3 - 2
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SThrow.java

@@ -42,8 +42,9 @@ public final class SThrow extends AStatement {
 
     @Override
     void analyze(ScriptRoot scriptRoot, Scope scope) {
-        expression.expected = Exception.class;
-        expression.analyze(scriptRoot, scope);
+        AExpression.Input expressionInput = new AExpression.Input();
+        expressionInput.expected = Exception.class;
+        expression.analyze(scriptRoot, scope, expressionInput);
         expression.cast();
 
         methodEscape = true;

+ 3 - 2
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SWhile.java

@@ -48,8 +48,9 @@ public final class SWhile extends AStatement {
     void analyze(ScriptRoot scriptRoot, Scope scope) {
         scope = scope.newLocalScope();
 
-        condition.expected = boolean.class;
-        condition.analyze(scriptRoot, scope);
+        AExpression.Input conditionInput = new AExpression.Input();
+        conditionInput.expected = boolean.class;
+        condition.analyze(scriptRoot, scope, conditionInput);
         condition.cast();
 
         if (condition instanceof EBoolean) {