You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by he...@apache.org on 2012/03/13 16:13:09 UTC

svn commit: r1300178 [2/3] - in /commons/proper/jexl/trunk: ./ src/main/java/org/apache/commons/jexl3/ src/main/java/org/apache/commons/jexl3/internal/ src/main/java/org/apache/commons/jexl3/internal/introspection/ src/main/java/org/apache/commons/jexl...

Modified: commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java
URL: http://svn.apache.org/viewvc/commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java?rev=1300178&r1=1300177&r2=1300178&view=diff
==============================================================================
--- commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java (original)
+++ commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java Tue Mar 13 15:13:08 2012
@@ -25,9 +25,9 @@ import org.apache.commons.jexl3.introspe
 import org.apache.commons.jexl3.introspection.JexlPropertyGet;
 import org.apache.commons.jexl3.introspection.JexlPropertySet;
 import org.apache.commons.jexl3.introspection.JexlUberspect;
-import org.apache.commons.jexl3.parser.ASTAdditiveNode;
-import org.apache.commons.jexl3.parser.ASTAdditiveOperator;
+import org.apache.commons.jexl3.parser.ASTAddNode;
 import org.apache.commons.jexl3.parser.ASTAndNode;
+import org.apache.commons.jexl3.parser.ASTArguments;
 import org.apache.commons.jexl3.parser.ASTArrayAccess;
 import org.apache.commons.jexl3.parser.ASTArrayLiteral;
 import org.apache.commons.jexl3.parser.ASTAssignment;
@@ -41,12 +41,14 @@ import org.apache.commons.jexl3.parser.A
 import org.apache.commons.jexl3.parser.ASTEQNode;
 import org.apache.commons.jexl3.parser.ASTERNode;
 import org.apache.commons.jexl3.parser.ASTEmptyFunction;
+import org.apache.commons.jexl3.parser.ASTEmptyMethod;
 import org.apache.commons.jexl3.parser.ASTFalseNode;
 import org.apache.commons.jexl3.parser.ASTForeachStatement;
 import org.apache.commons.jexl3.parser.ASTFunctionNode;
 import org.apache.commons.jexl3.parser.ASTGENode;
 import org.apache.commons.jexl3.parser.ASTGTNode;
 import org.apache.commons.jexl3.parser.ASTIdentifier;
+import org.apache.commons.jexl3.parser.ASTIdentifierAccess;
 import org.apache.commons.jexl3.parser.ASTIfStatement;
 import org.apache.commons.jexl3.parser.ASTJexlLambda;
 import org.apache.commons.jexl3.parser.ASTJexlScript;
@@ -69,6 +71,7 @@ import org.apache.commons.jexl3.parser.A
 import org.apache.commons.jexl3.parser.ASTSizeFunction;
 import org.apache.commons.jexl3.parser.ASTSizeMethod;
 import org.apache.commons.jexl3.parser.ASTStringLiteral;
+import org.apache.commons.jexl3.parser.ASTSubNode;
 import org.apache.commons.jexl3.parser.ASTTernaryNode;
 import org.apache.commons.jexl3.parser.ASTTrueNode;
 import org.apache.commons.jexl3.parser.ASTUnaryMinusNode;
@@ -103,12 +106,14 @@ public class Interpreter extends ParserV
     protected final JexlUberspect uberspect;
     /** The arithmetic handler. */
     protected final JexlArithmetic arithmetic;
-    /** The map of registered functions. */
+    /** The map of symboled functions. */
     protected final Map<String, Object> functions;
-    /** The map of registered functions. */
+    /** The map of symboled functions. */
     protected Map<String, Object> functors;
     /** The context to store/retrieve variables. */
     protected final JexlContext context;
+    /** The context to store/retrieve variables. */
+    protected final JexlContext.NamespaceResolver ns;
     /** Strict interpreter flag. */
     protected final boolean strictEngine;
     /** Strict interpreter flag. */
@@ -117,7 +122,7 @@ public class Interpreter extends ParserV
     protected final boolean silent;
     /** Cache executors. */
     protected final boolean cache;
-    /** Registers or arguments. */
+    /** symbol values. */
     protected final Scope.Frame frame;
     /** Cancellation support. */
     protected volatile boolean cancelled = false;
@@ -147,6 +152,11 @@ public class Interpreter extends ParserV
             this.silent = jexl.isSilent();
             this.arithmetic = jexl.arithmetic;
         }
+        if (this.context instanceof JexlContext.NamespaceResolver) {
+            ns = ((JexlContext.NamespaceResolver) context);
+        } else {
+            ns = Engine.EMPTY_NS;
+        }
         this.functions = jexl.functions;
         this.strictArithmetic = this.arithmetic.isStrict();
         this.cache = jexl.cache != null;
@@ -155,10 +165,8 @@ public class Interpreter extends ParserV
     }
 
     /**
-     * Interpret the given script/expression.
-     * <p>
-     * If the underlying JEXL engine is silent, errors will be logged through its logger as info.
-     * </p>
+     * Interpret the given script/expression. <p> If the underlying JEXL engine is silent, errors will be logged through
+     * its logger as info. </p>
      * @param node the script or expression to interpret.
      * @return the result of the interpretation.
      * @throws JexlException if any error occurs during interpretation.
@@ -243,15 +251,14 @@ public class Interpreter extends ParserV
     }
 
     /**
-     * Resolves a namespace, eventually allocating an instance using context as constructor argument.
-     * The lifetime of such instances span the current expression or script evaluation.
-     *
+     * Resolves a namespace, eventually allocating an instance using context as constructor argument. <p>The lifetime of
+     * such instances span the current expression or script evaluation.</p>
      * @param prefix the prefix name (may be null for global namespace)
      * @param node the AST node
      * @return the namespace instance
      */
     protected Object resolveNamespace(String prefix, JexlNode node) {
-        Object namespace = null;
+        Object namespace;
         // check whether this namespace is a functor
         if (functors != null) {
             namespace = functors.get(prefix);
@@ -260,9 +267,7 @@ public class Interpreter extends ParserV
             }
         }
         // check if namespace if a resolver
-        if (context instanceof JexlContext.NamespaceResolver) {
-            namespace = ((JexlContext.NamespaceResolver) context).resolveNamespace(prefix);
-        }
+        namespace = ns.resolveNamespace(prefix);
         if (namespace == null) {
             namespace = functions.get(prefix);
             if (prefix != null && namespace == null) {
@@ -289,48 +294,13 @@ public class Interpreter extends ParserV
     }
 
     @Override
-    protected Object visit(ASTAdditiveNode node, Object data) {
+    protected Object visit(ASTAndNode node, Object data) {
         /**
-         * The pattern for exception mgmt is to let the child*.jjtAccept
-         * out of the try/catch loop so that if one fails, the ex will
-         * traverse up to the interpreter.
-         * In cases where this is not convenient/possible, JexlException must
-         * be caught explicitly and rethrown.
+         * The pattern for exception mgmt is to let the child*.jjtAccept out of the try/catch loop so that if one fails,
+         * the ex will traverse up to the interpreter. In cases where this is not convenient/possible, JexlException
+         * must be caught explicitly and rethrown.
          */
         Object left = node.jjtGetChild(0).jjtAccept(this, data);
-        for (int c = 2, size = node.jjtGetNumChildren(); c < size; c += 2) {
-            Object right = node.jjtGetChild(c).jjtAccept(this, data);
-            try {
-                JexlNode op = node.jjtGetChild(c - 1);
-                if (op instanceof ASTAdditiveOperator) {
-                    String which = op.image;
-                    if ("+".equals(which)) {
-                        left = arithmetic.add(left, right);
-                        continue;
-                    }
-                    if ("-".equals(which)) {
-                        left = arithmetic.subtract(left, right);
-                        continue;
-                    }
-                    throw new UnsupportedOperationException("unknown operator " + which);
-                }
-                throw new IllegalArgumentException("unknown operator " + op);
-            } catch (ArithmeticException xrt) {
-                JexlNode xnode = findNullOperand(xrt, node, left, right);
-                throw new JexlException(xnode, "+/- error", xrt);
-            }
-        }
-        return left;
-    }
-
-    @Override
-    protected Object visit(ASTAdditiveOperator node, Object data) {
-        throw new UnsupportedOperationException("Shoud not be called.");
-    }
-
-    @Override
-    protected Object visit(ASTAndNode node, Object data) {
-        Object left = node.jjtGetChild(0).jjtAccept(this, data);
         try {
             boolean leftValue = arithmetic.toBoolean(left);
             if (!leftValue) {
@@ -352,25 +322,6 @@ public class Interpreter extends ParserV
     }
 
     @Override
-    protected Object visit(ASTArrayAccess node, Object data) {
-        // first objectNode is the identifier
-        Object object = node.jjtGetChild(0).jjtAccept(this, data);
-        // can have multiple nodes - either an expression, integer literal or reference
-        int numChildren = node.jjtGetNumChildren();
-        for (int i = 1; i < numChildren; i++) {
-            JexlNode nindex = node.jjtGetChild(i);
-            if (nindex instanceof JexlNode.Literal<?>) {
-                object = nindex.jjtAccept(this, object);
-            } else {
-                Object index = nindex.jjtAccept(this, null);
-                object = getAttribute(object, index, nindex);
-            }
-        }
-
-        return object;
-    }
-
-    @Override
     protected Object visit(ASTArrayLiteral node, Object data) {
         Object literal = node.getLiteral();
         if (literal == null) {
@@ -387,136 +338,25 @@ public class Interpreter extends ParserV
     }
 
     @Override
-    protected Object visit(ASTAssignment node, Object data) {
-        // left contains the reference to assign to
-        int register = -1;
-        JexlNode left = node.jjtGetChild(0);
-        if (left instanceof ASTIdentifier) {
-            ASTIdentifier var = (ASTIdentifier) left;
-            register = var.getRegister();
-            if (register < 0) {
-                throw new JexlException(left, "unknown variable " + left.image);
-            }
-        } else if (!(left instanceof ASTReference)) {
-            throw new JexlException(left, "illegal assignment form 0");
-        }
-        // right is the value expression to assign
+    protected Object visit(ASTAddNode node, Object data) {
+        Object left = node.jjtGetChild(0).jjtAccept(this, data);
         Object right = node.jjtGetChild(1).jjtAccept(this, data);
-
-        // determine initial object & property:
-        JexlNode objectNode = null;
-        Object object = register >= 0 ? frame.get(register) : null;
-        JexlNode propertyNode = null;
-        Object property = null;
-        boolean isVariable = true;
-        int v = 0;
-        StringBuilder variableName = null;
-        // 1: follow children till penultimate, resolve dot/array
-        int last = left.jjtGetNumChildren() - 1;
-        // check we are not assigning a register itself
-        boolean isRegister = last < 0 && register >= 0;
-        // start at 1 if register
-        for (int c = register >= 0 ? 1 : 0; c < last; ++c) {
-            objectNode = left.jjtGetChild(c);
-            // evaluate the property within the object
-            object = objectNode.jjtAccept(this, object);
-            if (object != null) {
-                continue;
-            }
-            isVariable &= objectNode instanceof ASTIdentifier
-                    || (objectNode instanceof ASTNumberLiteral && ((ASTNumberLiteral) objectNode).isInteger());
-            // if we get null back as a result, check for an ant variable
-            if (isVariable) {
-                if (v == 0) {
-                    variableName = new StringBuilder(left.jjtGetChild(0).image);
-                    v = 1;
-                }
-                for (; v <= c; ++v) {
-                    variableName.append('.');
-                    variableName.append(left.jjtGetChild(v).image);
-                }
-                object = context.get(variableName.toString());
-                // disallow mixing ant & bean with same root; avoid ambiguity
-                if (object != null) {
-                    isVariable = false;
-                }
-            } else {
-                throw new JexlException(objectNode, "illegal assignment form");
-            }
-        }
-        // 2: last objectNode will perform assignement in all cases
-        propertyNode = isRegister ? null : left.jjtGetChild(last);
-        boolean antVar = false;
-        if (propertyNode instanceof ASTIdentifier) {
-            ASTIdentifier identifier = (ASTIdentifier) propertyNode;
-            register = identifier.getRegister();
-            if (register >= 0) {
-                isRegister = true;
-            } else {
-                property = identifier.image;
-                antVar = true;
-            }
-        } else if (propertyNode instanceof ASTNumberLiteral && ((ASTNumberLiteral) propertyNode).isInteger()) {
-            property = ((ASTNumberLiteral) propertyNode).getLiteral();
-            antVar = true;
-        } else if (propertyNode instanceof ASTArrayAccess) {
-            // first objectNode is the identifier
-            objectNode = propertyNode;
-            ASTArrayAccess narray = (ASTArrayAccess) objectNode;
-            Object nobject = narray.jjtGetChild(0).jjtAccept(this, object);
-            if (nobject == null) {
-                throw new JexlException(objectNode, "array element is null");
-            } else {
-                object = nobject;
-            }
-            // can have multiple nodes - either an expression, integer literal or
-            // reference
-            last = narray.jjtGetNumChildren() - 1;
-            for (int i = 1; i < last; i++) {
-                objectNode = narray.jjtGetChild(i);
-                if (objectNode instanceof JexlNode.Literal<?>) {
-                    object = objectNode.jjtAccept(this, object);
-                } else {
-                    Object index = objectNode.jjtAccept(this, null);
-                    object = getAttribute(object, index, objectNode);
-                }
-            }
-            property = narray.jjtGetChild(last).jjtAccept(this, null);
-        } else if (!isRegister) {
-            throw new JexlException(objectNode, "illegal assignment form");
-        }
-        // deal with ant variable; set context
-        if (isRegister) {
-            frame.set(register, right);
-            return right;
-        } else if (antVar) {
-            if (isVariable && object == null) {
-                if (variableName != null) {
-                    if (last > 0) {
-                        variableName.append('.');
-                    }
-                    variableName.append(property);
-                    property = variableName.toString();
-                }
-                try {
-                    context.set(String.valueOf(property), right);
-                } catch (UnsupportedOperationException xsupport) {
-                    throw new JexlException(node, "context is readonly", xsupport);
-                }
-                return right;
-            }
-        }
-        if (property == null) {
-            // no property, we fail
-            throw new JexlException(propertyNode, "property is null");
+        try {
+            return arithmetic.add(left, right);
+        } catch (ArithmeticException xrt) {
+            throw new JexlException(node, "+ error", xrt);
         }
-        if (object == null) {
-            // no object, we fail
-            throw new JexlException(objectNode, "bean is null");
+    }
+
+    @Override
+    protected Object visit(ASTSubNode node, Object data) {
+        Object left = node.jjtGetChild(0).jjtAccept(this, data);
+        Object right = node.jjtGetChild(1).jjtAccept(this, data);
+        try {
+            return arithmetic.subtract(left, right);
+        } catch (ArithmeticException xrt) {
+            throw new JexlException(node, "- error", xrt);
         }
-        // one before last, assign
-        setAttribute(object, property, right, propertyNode);
-        return right;
     }
 
     @Override
@@ -588,28 +428,6 @@ public class Interpreter extends ParserV
     }
 
     @Override
-    protected Object visit(ASTEmptyFunction node, Object data) {
-        Object o = node.jjtGetChild(0).jjtAccept(this, data);
-        if (o == null) {
-            return Boolean.TRUE;
-        }
-        if (o instanceof String && "".equals(o)) {
-            return Boolean.TRUE;
-        }
-        if (o.getClass().isArray() && ((Object[]) o).length == 0) {
-            return Boolean.TRUE;
-        }
-        if (o instanceof Collection<?>) {
-            return ((Collection<?>) o).isEmpty() ? Boolean.TRUE : Boolean.FALSE;
-        }
-        // Map isn't a collection
-        if (o instanceof Map<?, ?>) {
-            return ((Map<?, ?>) o).isEmpty() ? Boolean.TRUE : Boolean.FALSE;
-        }
-        return Boolean.FALSE;
-    }
-
-    @Override
     protected Object visit(ASTEQNode node, Object data) {
         Object left = node.jjtGetChild(0).jjtAccept(this, data);
         Object right = node.jjtGetChild(1).jjtAccept(this, data);
@@ -631,7 +449,7 @@ public class Interpreter extends ParserV
         /* first objectNode is the loop variable */
         ASTReference loopReference = (ASTReference) node.jjtGetChild(0);
         ASTIdentifier loopVariable = (ASTIdentifier) loopReference.jjtGetChild(0);
-        int register = loopVariable.getRegister();
+        int symbol = loopVariable.getSymbol();
         /* second objectNode is the variable to iterate */
         Object iterableValue = node.jjtGetChild(1).jjtAccept(this, data);
         // make sure there is a value to iterate on and a statement to execute
@@ -648,10 +466,10 @@ public class Interpreter extends ParserV
                     }
                     // set loopVariable to value of iterator
                     Object value = itemsIterator.next();
-                    if (register < 0) {
+                    if (symbol < 0) {
                         context.set(loopVariable.image, value);
                     } else {
-                        frame.set(register, value);
+                        frame.set(symbol, value);
                     }
                     // execute statement
                     result = statement.jjtAccept(this, data);
@@ -741,52 +559,22 @@ public class Interpreter extends ParserV
     }
 
     @Override
-    protected Object visit(ASTIdentifier node, Object data) {
-        if (isCancelled()) {
-            throw new JexlException.Cancel(node);
-        }
-        String name = node.image;
-        if (data == null) {
-            int register = node.getRegister();
-            if (register >= 0) {
-                return frame.get(register);
-            }
-            Object value = context.get(name);
-            if (value == null
-                    && !(node.jjtGetParent() instanceof ASTReference)
-                    && !context.has(name)
-                    && !isTernaryProtected(node)) {
-                JexlException xjexl = new JexlException.Variable(node, name);
-                return unknownVariable(xjexl);
-            }
-            return value;
-        } else {
-            return getAttribute(data, name, node);
-        }
-    }
-
-    @Override
-    protected Object visit(ASTVar node, Object data) {
-        return visit((ASTIdentifier) node, data);
-    }
-
-    @Override
     protected Object visit(ASTIfStatement node, Object data) {
         int n = 0;
         try {
             Object result = null;
-            /* first objectNode is the condition */
-            Object expression = node.jjtGetChild(0).jjtAccept(this, data);
+            // first objectNode is the condition
+            Object expression = node.jjtGetChild(0).jjtAccept(this, null);
             if (arithmetic.toBoolean(expression)) {
                 // first objectNode is true statement
                 n = 1;
-                result = node.jjtGetChild(1).jjtAccept(this, data);
+                result = node.jjtGetChild(n).jjtAccept(this, null);
             } else {
                 // if there is a false, execute it. false statement is the second
                 // objectNode
                 if (node.jjtGetNumChildren() == 3) {
                     n = 2;
-                    result = node.jjtGetChild(2).jjtAccept(this, data);
+                    result = node.jjtGetChild(n).jjtAccept(this, null);
                 }
             }
             return result;
@@ -806,21 +594,6 @@ public class Interpreter extends ParserV
     }
 
     @Override
-    protected Object visit(ASTJexlScript node, Object data) {
-        if (node instanceof ASTJexlLambda) {
-            return new Closure(this, (ASTJexlLambda) node);
-        } else {
-            final int numChildren = node.jjtGetNumChildren();
-            Object result = null;
-            for (int i = 0; i < numChildren; i++) {
-                JexlNode child = node.jjtGetChild(i);
-                result = child.jjtAccept(this, data);
-            }
-            return result;
-        }
-    }
-
-    @Override
     protected Object visit(ASTLENode node, Object data) {
         Object left = node.jjtGetChild(0).jjtAccept(this, data);
         Object right = node.jjtGetChild(1).jjtAccept(this, data);
@@ -862,209 +635,30 @@ public class Interpreter extends ParserV
         return map;
     }
 
-    /**
-     * Calls a method (or function).
-     * <p>
-     * Method resolution is a follows:
-     * 1 - attempt to find a method in the bean passed as parameter;
-     * 2 - if this fails, narrow the arguments and try again
-     * 3 - if this still fails, seeks a JexlScript or JexlMethod as a property of that bean.
-     * </p>
-     * @param node the method node
-     * @param bean the bean this method should be invoked upon
-     * @param methodNode the node carrying the method name
-     * @param argb the first argument index, child of the method node
-     * @return the result of the method invocation
-     */
-    private Object call(JexlNode node, Object bean, ASTIdentifier methodNode, int argb) {
-        if (isCancelled()) {
-            throw new JexlException.Cancel(node);
-        }
-        String methodName = methodNode.image;
-        // evaluate the arguments
-        int argc = node.jjtGetNumChildren() - argb;
-        Object[] argv = new Object[argc];
-        for (int i = 0; i < argc; i++) {
-            argv[i] = node.jjtGetChild(i + argb).jjtAccept(this, null);
-        }
-
-        JexlException xjexl = null;
+    @Override
+    protected Object visit(ASTModNode node, Object data) {
+        Object left = node.jjtGetChild(0).jjtAccept(this, data);
+        Object right = node.jjtGetChild(1).jjtAccept(this, data);
         try {
-            // attempt to reuse last executor cached in volatile JexlNode.value
-            if (cache) {
-                Object cached = node.jjtGetValue();
-                if (cached instanceof JexlMethod) {
-                    JexlMethod me = (JexlMethod) cached;
-                    Object eval = me.tryInvoke(methodName, bean, argv);
-                    if (!me.tryFailed(eval)) {
-                        return eval;
-                    }
-                }
-            }
-            boolean cacheable = cache;
-            boolean narrow = true;
-            JexlMethod vm = null;
-            // pseudo loop
-            while (true) {
-                vm = uberspect.getMethod(bean, methodName, argv);
-                if (vm != null) {
-                    break;
-                }
-                Object functor = null;
-                // could not find a method, try as a var
-                if (bean == context) {
-                    int register = methodNode.getRegister();
-                    if (register >= 0) {
-                        functor = frame.get(register);
-                    } else {
-                        functor = context.get(methodName);
-                    }
-                } else {
-                    JexlPropertyGet gfunctor = uberspect.getPropertyGet(bean, methodName);
-                    if (gfunctor != null) {
-                        functor = gfunctor.tryInvoke(bean, methodName);
-                    }
-                }
-                // lambda, script or jexl method will do
-                if (functor instanceof JexlScript) {
-                    return ((JexlScript) functor).execute(context, argv);
-                }
-                if (functor instanceof JexlMethod) {
-                    return ((JexlMethod) functor).invoke(bean, argv);
-                }
-                // if we did not find an exact match and we haven't tried yet,
-                // attempt to narrow the parameters and if this succeeds, try again
-                if (narrow && arithmetic.narrowArguments(argv)) {
-                    narrow = false;
-                } else {
-                    break;
-                }
-            }
-            // we have either evaluated and returned or might have found a method
-            if (vm != null) {
-                // vm cannot be null if xjexl is null
-                Object eval = vm.invoke(bean, argv);
-                // cache executor in volatile JexlNode.value
-                if (cacheable && vm.isCacheable()) {
-                    node.jjtSetValue(vm);
-                }
-                return eval;
-            } else {
-                xjexl = new JexlException.Method(node, methodName, null);
+            return arithmetic.mod(left, right);
+        } catch (ArithmeticException xrt) {
+            if (!strictArithmetic) {
+                return new Double(0.0);
             }
-        } catch (Exception xany) {
-            xjexl = new JexlException(node, methodName, xany);
+            JexlNode xnode = findNullOperand(xrt, node, left, right);
+            throw new JexlException(xnode, "% error", xrt);
         }
-        return invocationFailed(xjexl);
     }
 
     @Override
-    protected Object visit(ASTMethodNode node, Object data) {
-        // the object to invoke the method on should be in the data argument
-        if (data == null) {
-            // if the method node is the first child of the (ASTReference) parent,
-            // it is considered as calling a 'top level' function
-            if (node.jjtGetParent().jjtGetChild(0) == node) {
-                data = resolveNamespace(null, node);
-                if (data == null) {
-                    data = context;
-                }
-            } else {
-                throw new JexlException(node, "attempting to call method on null");
-            }
-        }
-        // objectNode 0 is the identifier (method name), the others are parameters.
-        ASTIdentifier methodNode = (ASTIdentifier) node.jjtGetChild(0);
-        return call(node, data, methodNode, 1);
-    }
-
-    @Override
-    protected Object visit(ASTFunctionNode node, Object data) {
-        // objectNode 0 is the prefix
-        String prefix = node.jjtGetChild(0).image;
-        Object namespace = resolveNamespace(prefix, node);
-        // objectNode 1 is the identifier , the others are parameters.
-        ASTIdentifier functionNode = (ASTIdentifier) node.jjtGetChild(1);
-        return call(node, namespace, functionNode, 2);
-    }
-
-    @Override
-    protected Object visit(ASTConstructorNode node, Object data) {
-        if (isCancelled()) {
-            throw new JexlException.Cancel(node);
-        }
-        // first child is class or class name
-        Object cobject = node.jjtGetChild(0).jjtAccept(this, data);
-        // get the ctor args
-        int argc = node.jjtGetNumChildren() - 1;
-        Object[] argv = new Object[argc];
-        for (int i = 0; i < argc; i++) {
-            argv[i] = node.jjtGetChild(i + 1).jjtAccept(this, null);
-        }
-
-        JexlException xjexl = null;
-        try {
-            // attempt to reuse last constructor cached in volatile JexlNode.value
-            if (cache) {
-                Object cached = node.jjtGetValue();
-                if (cached instanceof JexlMethod) {
-                    JexlMethod mctor = (JexlMethod) cached;
-                    Object eval = mctor.tryInvoke(null, cobject, argv);
-                    if (!mctor.tryFailed(eval)) {
-                        return eval;
-                    }
-                }
-            }
-            JexlMethod ctor = uberspect.getConstructor(cobject, argv);
-            // DG: If we can't find an exact match, narrow the parameters and try again
-            if (ctor == null) {
-                if (arithmetic.narrowArguments(argv)) {
-                    ctor = uberspect.getConstructor(cobject, argv);
-                }
-                if (ctor == null) {
-                    String dbgStr = cobject != null ? cobject.toString() : null;
-                    xjexl = new JexlException.Method(node, dbgStr, null);
-                }
-            }
-            if (xjexl == null) {
-                Object instance = ctor.invoke(cobject, argv);
-                // cache executor in volatile JexlNode.value
-                if (cache && ctor.isCacheable()) {
-                    node.jjtSetValue(ctor);
-                }
-                return instance;
-            }
-        } catch (Exception xany) {
-            String dbgStr = cobject != null ? cobject.toString() : null;
-            xjexl = new JexlException(node, dbgStr, xany);
-        }
-        return invocationFailed(xjexl);
-    }
-
-    @Override
-    protected Object visit(ASTModNode node, Object data) {
-        Object left = node.jjtGetChild(0).jjtAccept(this, data);
-        Object right = node.jjtGetChild(1).jjtAccept(this, data);
-        try {
-            return arithmetic.mod(left, right);
-        } catch (ArithmeticException xrt) {
-            if (!strictArithmetic) {
-                return new Double(0.0);
-            }
-            JexlNode xnode = findNullOperand(xrt, node, left, right);
-            throw new JexlException(xnode, "% error", xrt);
-        }
-    }
-
-    @Override
-    protected Object visit(ASTMulNode node, Object data) {
-        Object left = node.jjtGetChild(0).jjtAccept(this, data);
-        Object right = node.jjtGetChild(1).jjtAccept(this, data);
-        try {
-            return arithmetic.multiply(left, right);
-        } catch (ArithmeticException xrt) {
-            JexlNode xnode = findNullOperand(xrt, node, left, right);
-            throw new JexlException(xnode, "* error", xrt);
+    protected Object visit(ASTMulNode node, Object data) {
+        Object left = node.jjtGetChild(0).jjtAccept(this, data);
+        Object right = node.jjtGetChild(1).jjtAccept(this, data);
+        try {
+            return arithmetic.multiply(left, right);
+        } catch (ArithmeticException xrt) {
+            JexlNode xnode = findNullOperand(xrt, node, left, right);
+            throw new JexlException(xnode, "* error", xrt);
         }
     }
 
@@ -1171,64 +765,8 @@ public class Interpreter extends ParserV
     }
 
     @Override
-    protected Object visit(ASTReference node, Object data) {
-        // could be array access, identifier or map literal
-        // followed by zero or more ("." and array access, method, size,
-        // identifier or integer literal)
-        int numChildren = node.jjtGetNumChildren();
-        // pass first piece of data in and loop through children
-        Object result = null;
-        StringBuilder variableName = null;
-        String propertyName = null;
-        boolean isVariable = true;
-        int v = 0;
-        for (int c = 0; c < numChildren; c++) {
-            if (isCancelled()) {
-                throw new JexlException.Cancel(node);
-            }
-            JexlNode theNode = node.jjtGetChild(c);
-            // integer literals may be part of an antish var name only if no bean was found so far
-            if (result == null && theNode instanceof ASTNumberLiteral && ((ASTNumberLiteral) theNode).isInteger()) {
-                isVariable &= v > 0;
-            } else {
-                isVariable &= (theNode instanceof ASTIdentifier);
-                result = theNode.jjtAccept(this, result);
-            }
-            // if we get null back a result, check for an ant variable
-            if (result == null && isVariable) {
-                if (v == 0) {
-                    variableName = new StringBuilder(node.jjtGetChild(0).image);
-                    v = 1;
-                }
-                for (; v <= c; ++v) {
-                    variableName.append('.');
-                    variableName.append(node.jjtGetChild(v).image);
-                }
-                result = context.get(variableName.toString());
-            } else {
-                propertyName = theNode.image;
-            }
-        }
-        if (result == null) {
-            if (isVariable && !isTernaryProtected(node)
-                    // variable unknow in context and not (from) a register
-                    && !(context.has(variableName.toString())
-                    || (numChildren == 1
-                    && node.jjtGetChild(0) instanceof ASTIdentifier
-                    && ((ASTIdentifier) node.jjtGetChild(0)).getRegister() >= 0))) {
-                JexlException xjexl = propertyName != null
-                                      ? new JexlException.Property(node, variableName.toString())
-                                      : new JexlException.Variable(node, variableName.toString());
-                return unknownVariable(xjexl);
-            }
-        }
-        return result;
-    }
-
-    @Override
     protected Object visit(ASTReferenceExpression node, Object data) {
-        ASTArrayAccess upper = node;
-        return visit(upper, data);
+        return node.jjtGetChild(0).jjtAccept(this, data);
     }
 
     @Override
@@ -1237,39 +775,6 @@ public class Interpreter extends ParserV
         throw new JexlException.Return(node, null, val);
     }
 
-    /**
-     * Check if a null evaluated expression is protected by a ternary expression.
-     * The rationale is that the ternary / elvis expressions are meant for the user to explictly take
-     * control over the error generation; ie, ternaries can return null even if the engine in isStrict mode
-     * would normally throw an exception.
-     * @param node the expression node
-     * @return true if nullable variable, false otherwise
-     */
-    private boolean isTernaryProtected(JexlNode node) {
-        for (JexlNode walk = node.jjtGetParent(); walk != null; walk = walk.jjtGetParent()) {
-            if (walk instanceof ASTTernaryNode) {
-                return true;
-            } else if (!(walk instanceof ASTReference || walk instanceof ASTArrayAccess)) {
-                break;
-            }
-        }
-        return false;
-    }
-
-    @Override
-    protected Object visit(ASTSizeFunction node, Object data) {
-        Object val = node.jjtGetChild(0).jjtAccept(this, data);
-        if (val == null) {
-            throw new JexlException(node, "size() : argument is null", null);
-        }
-        return Integer.valueOf(sizeOf(node, val));
-    }
-
-    @Override
-    protected Object visit(ASTSizeMethod node, Object data) {
-        return Integer.valueOf(sizeOf(node, data));
-    }
-
     @Override
     protected Object visit(ASTStringLiteral node, Object data) {
         if (data != null) {
@@ -1333,108 +838,687 @@ public class Interpreter extends ParserV
         return result;
     }
 
+    @Override
+    protected Object visit(ASTSizeFunction node, Object data) {
+        Object val = node.jjtGetChild(0).jjtAccept(this, data);
+        if (val == null) {
+            throw new JexlException(node, "size() : argument is null", null);
+        }
+        return Integer.valueOf(sizeOf(node, val));
+    }
+
+    @Override
+    protected Object visit(ASTSizeMethod node, Object data) {
+        Object val = node.jjtGetChild(0).jjtAccept(this, data);
+        return Integer.valueOf(sizeOf(node, val));
+    }
+
+    @Override
+    protected Object visit(ASTEmptyFunction node, Object data) {
+        return isEmpty(node, node.jjtGetChild(0).jjtAccept(this, data));
+    }
+
+    @Override
+    protected Object visit(ASTEmptyMethod node, Object data) {
+        Object val = node.jjtGetChild(0).jjtAccept(this, data);
+        return isEmpty(node, val);
+    }
+
+    /**
+     * Check for emptyness of various types: Collection, Array, Map, String, and anything that has a boolean isEmpty()
+     * method.
+     *
+     * @param node the node holding the object
+     * @param object the object to check the rmptyness of.
+     * @return the boolean
+     */
+    private Boolean isEmpty(JexlNode node, Object object) {
+        if (object == null) {
+            return Boolean.TRUE;
+        }
+        if (object instanceof Number) {
+            return ((Number) object).intValue() == 0 ? Boolean.TRUE : Boolean.FALSE;
+        }
+        if (object instanceof String) {
+            return "".equals(object) ? Boolean.TRUE : Boolean.FALSE;
+        }
+        if (object.getClass().isArray()) {
+            return ((Object[]) object).length == 0 ? Boolean.TRUE : Boolean.FALSE;
+        }
+        if (object instanceof Collection<?>) {
+            return ((Collection<?>) object).isEmpty() ? Boolean.TRUE : Boolean.FALSE;
+        }
+        // Map isn't a collection
+        if (object instanceof Map<?, ?>) {
+            return ((Map<?, ?>) object).isEmpty() ? Boolean.TRUE : Boolean.FALSE;
+        } else {
+            // check if there is an isEmpty method on the object that returns a
+            // boolean and if so, just use it
+            Object[] params = new Object[0];
+            JexlMethod vm = uberspect.getMethod(object, "isEmpty", EMPTY_PARAMS);
+            if (vm != null && vm.getReturnType() == Boolean.TYPE) {
+                Boolean result;
+                try {
+                    result = (Boolean) vm.invoke(object, params);
+                } catch (Exception e) {
+                    throw new JexlException(node, "empty() : error executing", e);
+                }
+                return result;
+            }
+            throw new JexlException(node, "empty() : unsupported type : " + object.getClass(), null);
+        }
+    }
+
     /**
-     * Calculate the <code>size</code> of various types: Collection, Array,
-     * Map, String, and anything that has a int size() method.
+     * Calculate the
+     * <code>size</code> of various types: Collection, Array, Map, String, and anything that has a int size() method.
+     *
      * @param node the node that gave the value to size
-     * @param val the object to get the size of.
+     * @param object the object to get the size of.
      * @return the size of val
      */
-    private int sizeOf(JexlNode node, Object val) {
-        if (val instanceof Collection<?>) {
-            return ((Collection<?>) val).size();
-        } else if (val.getClass().isArray()) {
-            return Array.getLength(val);
-        } else if (val instanceof Map<?, ?>) {
-            return ((Map<?, ?>) val).size();
-        } else if (val instanceof String) {
-            return ((String) val).length();
+    private int sizeOf(JexlNode node, Object object) {
+        if (object == null) {
+            return 0;
+        } else if (object instanceof Collection<?>) {
+            return ((Collection<?>) object).size();
+        } else if (object.getClass().isArray()) {
+            return Array.getLength(object);
+        } else if (object instanceof Map<?, ?>) {
+            return ((Map<?, ?>) object).size();
+        } else if (object instanceof String) {
+            return ((String) object).length();
         } else {
             // check if there is a size method on the object that returns an
             // integer and if so, just use it
             Object[] params = new Object[0];
-            JexlMethod vm = uberspect.getMethod(val, "size", EMPTY_PARAMS);
+            JexlMethod vm = uberspect.getMethod(object, "size", EMPTY_PARAMS);
             if (vm != null && vm.getReturnType() == Integer.TYPE) {
                 Integer result;
                 try {
-                    result = (Integer) vm.invoke(val, params);
+                    result = (Integer) vm.invoke(object, params);
                 } catch (Exception e) {
                     throw new JexlException(node, "size() : error executing", e);
                 }
                 return result.intValue();
             }
-            throw new JexlException(node, "size() : unsupported type : " + val.getClass(), null);
+            throw new JexlException(node, "size() : unsupported type : " + object.getClass(), null);
         }
     }
 
-    /**
-     * Gets an attribute of an object.
-     *
-     * @param object to retrieve value from
-     * @param attribute the attribute of the object, e.g. an index (1, 0, 2) or
-     *            key for a map
-     * @return the attribute value
-     */
-    public Object getAttribute(Object object, Object attribute) {
-        return getAttribute(object, attribute, null);
+    @Override
+    protected Object visit(ASTJexlScript node, Object data) {
+        if (node instanceof ASTJexlLambda) {
+            return new Closure(this, (ASTJexlLambda) node);
+        } else {
+            final int numChildren = node.jjtGetNumChildren();
+            Object result = null;
+            for (int i = 0; i < numChildren; i++) {
+                JexlNode child = node.jjtGetChild(i);
+                result = child.jjtAccept(this, data);
+            }
+            return result;
+        }
     }
 
-    /**
-     * Gets an attribute of an object.
-     *
-     * @param object to retrieve value from
-     * @param attribute the attribute of the object, e.g. an index (1, 0, 2) or
-     *            key for a map
-     * @param node the node that evaluated as the object
-     * @return the attribute value
-     */
-    protected Object getAttribute(Object object, Object attribute, JexlNode node) {
-        if (object == null) {
-            throw new JexlException(node, "object is null");
-        }
+    @Override
+    protected Object visit(ASTIdentifier node, Object data) {
         if (isCancelled()) {
             throw new JexlException.Cancel(node);
         }
-        // attempt to reuse last executor cached in volatile JexlNode.value
-        if (node != null && cache) {
-            Object cached = node.jjtGetValue();
-            if (cached instanceof JexlPropertyGet) {
-                JexlPropertyGet vg = (JexlPropertyGet) cached;
-                Object value = vg.tryInvoke(object, attribute);
-                if (!vg.tryFailed(value)) {
-                    return value;
-                }
+        String name = node.image;
+        if (data == null) {
+            int symbol = node.getSymbol();
+            if (symbol >= 0) {
+                return frame.get(symbol);
             }
-        }
-        JexlPropertyGet vg = uberspect.getPropertyGet(object, attribute);
-        if (vg != null) {
-            try {
-                Object value = vg.invoke(object);
-                // cache executor in volatile JexlNode.value
-                if (node != null && cache && vg.isCacheable()) {
-                    node.jjtSetValue(vg);
-                }
-                return value;
-            } catch (Exception xany) {
-                String attrStr = attribute != null ? attribute.toString() : null;
-                JexlException xjexl = new JexlException.Property(node, attrStr, xany);
-                if (strictArithmetic) {
-                    throw xjexl;
-                }
-                if (!silent) {
-                    logger.warn(xjexl.getMessage());
-                }
+            Object value = context.get(name);
+            if (value == null
+                    && !(node.jjtGetParent() instanceof ASTReference)
+                    && !context.has(name)
+                    && !isTernaryProtected(node)) {
+                JexlException xjexl = new JexlException.Variable(node, name);
+                return unknownVariable(xjexl);
             }
+            return value;
+        } else {
+            return getAttribute(data, name, node);
         }
-        return null;
     }
 
-    /**
-     * Sets an attribute of an object.
-     *
+    @Override
+    protected Object visit(ASTVar node, Object data) {
+        return visit((ASTIdentifier) node, data);
+    }
+
+    @Override
+    protected Object visit(ASTArrayAccess node, Object data) {
+        // first objectNode is the identifier
+        Object object = data;
+        // can have multiple nodes - either an expression, integer literal or reference
+        int numChildren = node.jjtGetNumChildren();
+        for (int i = 0; i < numChildren; i++) {
+            JexlNode nindex = node.jjtGetChild(i);
+            if (object == null) {
+                return null;
+            }
+            Object index = nindex.jjtAccept(this, null);
+            object = getAttribute(object, index, nindex);
+        }
+        return object;
+    }
+
+    @Override
+    protected Object visit(ASTIdentifierAccess node, Object data) {
+        // child 0 is the identifier, data is the object
+        return data != null ? getAttribute(data, node.getIdentifier(), node) : null;
+    }
+
+    /**
+     * Check if a null evaluated expression is protected by a ternary expression.
+     * <p>
+     * The rationale is that the ternary / elvis expressions are meant for the user to explictly take control
+     * over the error generation; ie, ternaries can return null even if the engine in isStrict mode
+     * would normally throw an exception.
+     * </p>
+     * @param node the expression node
+     * @return true if nullable variable, false otherwise
+     */
+    private boolean isTernaryProtected(JexlNode node) {
+        for (JexlNode walk = node.jjtGetParent(); walk != null; walk = walk.jjtGetParent()) {
+            if (walk instanceof ASTTernaryNode) {
+                return true;
+            } else if (!(walk instanceof ASTReference || walk instanceof ASTArrayAccess)) {
+                break;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks whether a reference child node holds a local variable reference.
+     * @param node the reference node
+     * @param which the child we are checking
+     * @return true if child is local variable, false otherwise
+     */
+    private boolean isLocalVariable(ASTReference node, int which) {
+        return (node.jjtGetNumChildren() > which
+                && node.jjtGetChild(which) instanceof ASTIdentifier
+                && ((ASTIdentifier) node.jjtGetChild(which)).getSymbol() >= 0);
+    }
+
+    @Override
+    protected Object visit(ASTReference node, Object data) {
+        final int numChildren = node.jjtGetNumChildren();
+        JexlNode parent = node.jjtGetParent();
+        // pass first piece of data in and loop through children
+        Object object = null;
+        JexlNode objectNode;
+        StringBuilder variableName = null;
+        boolean isVariable = !(parent instanceof ASTReference);
+        int v = 0;
+        main:
+        for (int c = 0; c < numChildren; c++) {
+            if (isCancelled()) {
+                throw new JexlException.Cancel(node);
+            }
+            objectNode = node.jjtGetChild(c);
+            // attempt to evaluate the property within the object
+            object = objectNode.jjtAccept(this, object);
+            if (object == null && isVariable) {
+                // if we still have a null object and we are evaluating 'x.y', check for an antish variable
+                if (v == 0) {
+                    // first node must be an Identifier
+                    if (objectNode instanceof ASTIdentifier) {
+                        variableName = new StringBuilder(objectNode.image);
+                        v = 1;
+                    } else {
+                        break main;
+                    }
+                }
+                for (; v <= c; ++v) {
+                    // subsequent nodes must be identifier access
+                    objectNode = node.jjtGetChild(v);
+                    if (objectNode instanceof ASTIdentifierAccess) {
+                        variableName.append('.');
+                        variableName.append(objectNode.image);
+                    } else {
+                        break main;
+                    }
+                }
+                object = context.get(variableName.toString());
+            }
+            isVariable &= object == null;
+        }
+        if (object == null && isVariable && variableName != null
+            && !isTernaryProtected(node) && !(context.has(variableName.toString()) || isLocalVariable(node, 0))) {
+            JexlException xjexl = new JexlException.Variable(node, variableName.toString());
+            // variable unknown in context and not a local
+            return unknownVariable(xjexl);
+        }
+        return object;
+    }
+
+    @Override
+    protected Object visit(ASTAssignment node, Object data) {
+        // left contains the reference to assign to
+        final JexlNode left = node.jjtGetChild(0);
+        // right is the value expression to assign
+        final Object right = node.jjtGetChild(1).jjtAccept(this, data);
+        Object object = null;
+        int symbol = -1;
+        // 0: determine initial object & property:
+        final int last = left.jjtGetNumChildren() - 1;
+        if (left instanceof ASTIdentifier) {
+            ASTIdentifier var = (ASTIdentifier) left;
+            symbol = var.getSymbol();
+            if (symbol >= 0) {
+                // check we are not assigning a symbol itself
+                if (last < 0) {
+                    frame.set(symbol, right);
+                    return right;
+                }
+                object = frame.get(symbol);
+            } else {
+                // check we are not assigning direct global
+                if (last < 0) {
+                    try {
+                        context.set(var.image, right);
+                    } catch (UnsupportedOperationException xsupport) {
+                        throw new JexlException(node, "context is readonly", xsupport);
+                    }
+                    return right;
+                }
+                object = context.get(var.image);
+            }
+        } else if (!(left instanceof ASTReference)) {
+            throw new JexlException(left, "illegal assignment form 0");
+        }
+        // 1: follow children till penultimate, resolve dot/array
+        JexlNode objectNode = null;
+        boolean isVariable = true;
+        int v = 0;
+        StringBuilder variableName = null;
+        // start at 1 if symbol
+        for (int c = symbol >= 0 ? 1 : 0; c < last; ++c) {
+            if (isCancelled()) {
+                throw new JexlException.Cancel(left);
+            }
+            objectNode = left.jjtGetChild(c);
+            object = objectNode.jjtAccept(this, object);
+            if (object != null) {
+                // disallow mixing antish variable & bean with same root; avoid ambiguity
+                isVariable = false;
+                continue;
+            }
+            // if we still have a null object, check for an antish variable
+            if (isVariable) {
+                if (v == 0) {
+                    if (objectNode instanceof ASTIdentifier) {
+                        variableName = new StringBuilder(objectNode.image);
+                        v = 1;
+                    } else {
+                        isVariable = false;
+                    }
+                }
+                for (; isVariable && v <= c; ++v) {
+                    JexlNode child = left.jjtGetChild(v);
+                    if (child instanceof ASTIdentifierAccess) {
+                        variableName.append('.');
+                        variableName.append(child.image);
+                    } else {
+                        isVariable = false;
+                    }
+                }
+                if (isVariable) {
+                    object = context.get(variableName.toString());
+                } else {
+                    break;
+                }
+            } else {
+                throw new JexlException(objectNode, "illegal assignment form");
+            }
+        }
+        // 2: last objectNode will perform assignement in all cases
+        Object property = null;
+        JexlNode propertyNode = left.jjtGetChild(last);
+        if (propertyNode instanceof ASTIdentifierAccess) {
+            property = ((ASTIdentifierAccess) propertyNode).getIdentifier();
+            // deal with antish variable
+            if (variableName != null && object == null) {
+                if (last > 0) {
+                    variableName.append('.');
+                }
+                variableName.append(String.valueOf(property));
+                try {
+                    context.set(variableName.toString(), right);
+                } catch (UnsupportedOperationException xsupport) {
+                    throw new JexlException(node, "context is readonly", xsupport);
+                }
+                return right;
+            }
+        } else if (propertyNode instanceof ASTArrayAccess) {
+            // can have multiple nodes - either an expression, integer literal or reference
+            int numChildren = propertyNode.jjtGetNumChildren() - 1;
+            for (int i = 0; i < numChildren; i++) {
+                JexlNode nindex = propertyNode.jjtGetChild(i);
+                Object index = nindex.jjtAccept(this, null);
+                object = getAttribute(object, index, nindex);
+            }
+            propertyNode = propertyNode.jjtGetChild(numChildren);
+            property = propertyNode.jjtAccept(this, null);
+        } else {
+            throw new JexlException(objectNode, "illegal assignment form");
+        }
+        if (property == null) {
+            // no property, we fail
+            throw new JexlException(propertyNode, "property is null");
+        }
+        if (object == null) {
+            // no object, we fail
+            throw new JexlException(objectNode, "bean is null");
+        }
+        // 3: one before last, assign
+        setAttribute(object, property, right, propertyNode);
+        return right;
+    }
+
+    @Override
+    protected Object[] visit(ASTArguments node, Object data) {
+        final int argc = node.jjtGetNumChildren();
+        final Object[] argv = new Object[argc];
+        for (int i = 0; i < argc; i++) {
+            argv[i] = node.jjtGetChild(i).jjtAccept(this, data);
+        }
+        return argv;
+    }
+
+    @Override
+    protected Object visit(final ASTMethodNode node, Object data) {
+        // left contains the reference to the method
+        final JexlNode methodNode = node.jjtGetChild(0);
+        Object object = null;
+        JexlNode objectNode = null;
+        Object method;
+        // 1: determine object and method or functor
+        if (methodNode instanceof ASTIdentifierAccess) {
+            method = methodNode;
+            object = data;
+            if (object == null) {
+                // no object, we fail
+                throw new JexlException(objectNode, "object is null");
+            }
+        } else {
+            method = methodNode.jjtAccept(this, null);
+        }
+        Object result = method;
+        for (int a = 1; a < node.jjtGetNumChildren(); ++a) {
+            if (result == null) {
+                // no method, we fail
+                throw new JexlException(methodNode, "method is null");
+            }
+            ASTArguments argNode = (ASTArguments) node.jjtGetChild(a);
+            result = call(node, object, result, argNode);
+            object = null;
+        }
+        return result;
+    }
+
+    /**
+     * Calls a method (or function).
+     * <p> Method resolution is a follows:
+     * 1 - attempt to find a method in the bean passed as parameter;
+     * 3 - if this fails, seeks a JexlScript or JexlMethod as a property of that bean;
+     * 2 - if this fails, narrow the arguments and try again
+     * </p>
+     *
+     * @param node the method node
+     * @param bean the bean this method should be invoked upon
+     * @param functor the object carrying the method or function
+     * @return the result of the method invocation
+     */
+    private Object call(JexlNode node, Object bean, Object functor, ASTArguments argNode) {
+        if (isCancelled()) {
+            throw new JexlException.Cancel(node);
+        }
+        JexlException xjexl;
+        // evaluate the arguments
+        Object[] argv = visit(argNode, null);
+
+        // get the method name if identifier
+        String methodName = null;
+        int symbol = -1;
+        if (functor instanceof ASTIdentifier) {
+            ASTIdentifier methodIdentifier = (ASTIdentifier) functor;
+            symbol = methodIdentifier.getSymbol();
+            if (symbol < 0) {
+                methodName = methodIdentifier.image;
+            }
+            functor = null;
+        } else if (functor instanceof ASTIdentifierAccess) {
+            methodName = ((ASTIdentifierAccess) functor).image;
+            functor = null;
+        }
+        try {
+            boolean cacheable = cache;
+            boolean narrow = true;
+            JexlMethod vm = null;
+            // pseudo loop and a half
+            while (true) {
+                if (methodName != null) {
+                    // attempt to reuse last executor cached in volatile JexlNode.value
+                    if (cache) {
+                        Object cached = node.jjtGetValue();
+                        if (cached instanceof JexlMethod) {
+                            JexlMethod me = (JexlMethod) cached;
+                            Object eval = me.tryInvoke(methodName, bean, argv);
+                            if (!me.tryFailed(eval)) {
+                                return eval;
+                            }
+                        }
+                    }
+                    // try a method
+                    vm = uberspect.getMethod(bean, methodName, argv);
+                    if (vm != null || !narrow) {
+                        // if there is a method name, we will exit here on first or second pass
+                        break;
+                    }
+                }
+                // could not find a method, try as a var
+                if (functor == null) {
+                    if (symbol >= 0) {
+                        functor = frame.get(symbol);
+                    } else {
+                        JexlPropertyGet get = uberspect.getPropertyGet(bean, methodName);
+                        if (get != null) {
+                            functor = get.tryInvoke(bean, methodName);
+                        }
+                    }
+                }
+                // lambda, script or jexl method will do
+                if (functor instanceof JexlScript) {
+                    return ((JexlScript) functor).execute(context, argv);
+                }
+                if (functor instanceof JexlMethod) {
+                    return ((JexlMethod) functor).invoke(bean, argv);
+                }
+                // if we did not find an exact method by name and we haven't tried yet,
+                // attempt to narrow the parameters and if this succeeds, try again in next loop
+                if (methodName == null || !arithmetic.narrowArguments(argv)) {
+                    break;
+                } else {
+                    narrow = false;
+                }
+            }
+            // we have either evaluated and returned or might have found a method
+            if (vm != null) {
+                // vm cannot be null if xjexl is null
+                Object eval = vm.invoke(bean, argv);
+                // cache executor in volatile JexlNode.value
+                if (cacheable && vm.isCacheable()) {
+                    node.jjtSetValue(vm);
+                }
+                return eval;
+            } else {
+                xjexl = new JexlException.Method(node, methodName, null);
+            }
+        } catch (Exception xany) {
+            xjexl = new JexlException(node, methodName, xany);
+        }
+        return invocationFailed(xjexl);
+    }
+
+    @Override
+    protected Object visit(ASTFunctionNode node, Object data) {
+        int argc = node.jjtGetNumChildren();
+        if (argc == 2) {
+            Object namespace = resolveNamespace(null, node);
+            if (namespace == null) {
+                namespace = context;
+            }
+            ASTIdentifier functionNode = (ASTIdentifier) node.jjtGetChild(0);
+            ASTArguments argNode = (ASTArguments) node.jjtGetChild(1);
+            return call(node, namespace, functionNode, argNode);
+        } else {
+            // objectNode 0 is the prefix
+            String prefix = node.jjtGetChild(0).image;
+            Object namespace = resolveNamespace(prefix, node);
+            // objectNode 1 is the identifier , the others are parameters.
+            ASTIdentifier functionNode = (ASTIdentifier) node.jjtGetChild(1);
+            ASTArguments argNode = (ASTArguments) node.jjtGetChild(2);
+            return call(node, namespace, functionNode, argNode);
+        }
+    }
+
+    @Override
+    protected Object visit(ASTConstructorNode node, Object data) {
+        if (isCancelled()) {
+            throw new JexlException.Cancel(node);
+        }
+        // first child is class or class name
+        Object cobject = node.jjtGetChild(0).jjtAccept(this, data);
+        // get the ctor args
+        int argc = node.jjtGetNumChildren() - 1;
+        Object[] argv = new Object[argc];
+        for (int i = 0; i < argc; i++) {
+            argv[i] = node.jjtGetChild(i + 1).jjtAccept(this, data);
+        }
+
+        JexlException xjexl = null;
+        try {
+            // attempt to reuse last constructor cached in volatile JexlNode.value
+            if (cache) {
+                Object cached = node.jjtGetValue();
+                if (cached instanceof JexlMethod) {
+                    JexlMethod mctor = (JexlMethod) cached;
+                    Object eval = mctor.tryInvoke(null, cobject, argv);
+                    if (!mctor.tryFailed(eval)) {
+                        return eval;
+                    }
+                }
+            }
+            JexlMethod ctor = uberspect.getConstructor(cobject, argv);
+            // DG: If we can't find an exact match, narrow the parameters and try again
+            if (ctor == null) {
+                if (arithmetic.narrowArguments(argv)) {
+                    ctor = uberspect.getConstructor(cobject, argv);
+                }
+                if (ctor == null) {
+                    String dbgStr = cobject != null ? cobject.toString() : null;
+                    xjexl = new JexlException.Method(node, dbgStr, null);
+                }
+            }
+            if (xjexl == null) {
+                Object instance = ctor.invoke(cobject, argv);
+                // cache executor in volatile JexlNode.value
+                if (cache && ctor.isCacheable()) {
+                    node.jjtSetValue(ctor);
+                }
+                return instance;
+            }
+        } catch (Exception xany) {
+            String dbgStr = cobject != null ? cobject.toString() : null;
+            xjexl = new JexlException(node, dbgStr, xany);
+        }
+        return invocationFailed(xjexl);
+    }
+
+    /**
+     * Gets an attribute of an object.
+     *
+     * @param object to retrieve value from
+     * @param attribute the attribute of the object, e.g. an index (1, 0, 2) or key for a map
+     * @return the attribute value
+     */
+    public Object getAttribute(Object object, Object attribute) {
+        return getAttribute(object, attribute, null);
+    }
+
+    /**
+     * Gets an attribute of an object.
+     *
+     * @param object to retrieve value from
+     * @param attribute the attribute of the object, e.g. an index (1, 0, 2) or key for a map
+     * @param node the node that evaluated as the object
+     * @return the attribute value
+     */
+    protected Object getAttribute(Object object, Object attribute, JexlNode node) {
+        if (object == null) {
+            throw new JexlException(node, "object is null");
+        }
+        if (isCancelled()) {
+            throw new JexlException.Cancel(node);
+        }
+        // attempt to reuse last executor cached in volatile JexlNode.value
+        if (node != null && cache) {
+            Object cached = node.jjtGetValue();
+            if (cached instanceof JexlPropertyGet) {
+                JexlPropertyGet vg = (JexlPropertyGet) cached;
+                Object value = vg.tryInvoke(object, attribute);
+                if (!vg.tryFailed(value)) {
+                    return value;
+                }
+            }
+        }
+        JexlException xjexl = null;
+        JexlPropertyGet vg = uberspect.getPropertyGet(object, attribute);
+        if (vg != null) {
+            try {
+                Object value = vg.invoke(object);
+                // cache executor in volatile JexlNode.value
+                if (node != null && cache && vg.isCacheable()) {
+                    node.jjtSetValue(vg);
+                }
+                return value;
+            } catch (Exception xany) {
+                String attrStr = attribute != null ? attribute.toString() : null;
+                xjexl = new JexlException.Property(node, attrStr, xany);
+            }
+        }
+        if (xjexl == null) {
+            if (node == null) {
+                String error = "unable to get object property"
+                        + ", class: " + object.getClass().getName()
+                        + ", property: " + attribute;
+                throw new UnsupportedOperationException(error);
+            }
+            String attrStr = attribute != null ? attribute.toString() : null;
+            xjexl = new JexlException.Property(node, attrStr, null);
+        }
+        if (strictEngine) {
+            throw xjexl;
+        }
+        if (!silent) {
+            logger.warn(xjexl.getMessage());
+        }
+        return null;
+    }
+
+    /**
+     * Sets an attribute of an object.
+     *
      * @param object to set the value to
-     * @param attribute the attribute of the object, e.g. an index (1, 0, 2) or
-     *            key for a map
+     * @param attribute the attribute of the object, e.g. an index (1, 0, 2) or key for a map
      * @param value the value to assign to the object's attribute
      */
     public void setAttribute(Object object, Object attribute, Object value) {
@@ -1445,8 +1529,7 @@ public class Interpreter extends ParserV
      * Sets an attribute of an object.
      *
      * @param object to set the value to
-     * @param attribute the attribute of the object, e.g. an index (1, 0, 2) or
-     *            key for a map
+     * @param attribute the attribute of the object, e.g. an index (1, 0, 2) or key for a map
      * @param value the value to assign to the object's attribute
      * @param node the node that evaluated as the object
      */
@@ -1483,16 +1566,16 @@ public class Interpreter extends ParserV
                     node.jjtSetValue(vs);
                 }
                 return;
-            } catch (RuntimeException xrt) {
-                if (node == null) {
-                    throw xrt;
-                }
-                xjexl = new JexlException(node, "set object property error", xrt);
             } catch (Exception xany) {
                 if (node == null) {
-                    throw new RuntimeException(xany);
+                    if (xany instanceof RuntimeException) {
+                        throw (RuntimeException) xany;
+                    } else {
+                        throw new RuntimeException(xany);
+                    }
                 }
-                xjexl = new JexlException(node, "set object property error", xany);
+                String attrStr = attribute != null ? attribute.toString() : null;
+                xjexl = new JexlException.Property(node, attrStr, xany);
             }
         }
         if (xjexl == null) {
@@ -1503,9 +1586,10 @@ public class Interpreter extends ParserV
                         + ", argument: " + value.getClass().getSimpleName();
                 throw new UnsupportedOperationException(error);
             }
-            xjexl = new JexlException.Property(node, attribute.toString());
+            String attrStr = attribute != null ? attribute.toString() : null;
+            xjexl = new JexlException.Property(node, attrStr, null);
         }
-        if (strictArithmetic) {
+        if (strictEngine) {
             throw xjexl;
         }
         if (!silent) {

Modified: commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/internal/Scope.java
URL: http://svn.apache.org/viewvc/commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/internal/Scope.java?rev=1300178&r1=1300177&r2=1300178&view=diff
==============================================================================
--- commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/internal/Scope.java (original)
+++ commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/internal/Scope.java Tue Mar 13 15:13:08 2012
@@ -20,7 +20,7 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 
 /**
- * A script scope, stores the declaration of parameters and local variables.
+ * A script scope, stores the declaration of parameters and local variables as symbols.
  * @since 3.0
  */
 public final class Scope {
@@ -37,8 +37,8 @@ public final class Scope {
      */
     private int vars;
     /**
-     * The map of named varialbes aka script parameters and local variables.
-     * Each parameter is associated to a register and is materialized as an offset in the stacked array used
+     * The map of named variables aka script parameters and local variables.
+     * Each parameter is associated to a symbol and is materialized as an offset in the stacked array used
      * during evaluation.
      */
     private Map<String, Integer> namedVariables = null;
@@ -94,25 +94,25 @@ public final class Scope {
     }
 
     /**
-     * Checks whether an identifier is a local variable or argument, ie stored in a register.
+     * Checks whether an identifier is a local variable or argument, ie a symbol.
      * If this fails, attempt to solve by hoisting parent stacked.
-     * @param name the register name
-     * @return the register index
+     * @param name the symbol name
+     * @return the symbol index
      */
-    public Integer getRegister(String name) {
-        return getRegister(name, true);
+    public Integer getSymbol(String name) {
+        return getSymbol(name, true);
     }
 
     /**
-     * Checks whether an identifier is a local variable or argument, ie stored in a register.
-     * @param name the register name
+     * Checks whether an identifier is a local variable or argument, ie a symbol.
+     * @param name the symbol name
      * @param hoist whether solving by hoisting parent stacked is allowed
-     * @return the register index
+     * @return the symbol index
      */
-    private Integer getRegister(String name, boolean hoist) {
+    private Integer getSymbol(String name, boolean hoist) {
         Integer register = namedVariables != null ? namedVariables.get(name) : null;
         if (register == null && hoist && parent != null) {
-            Integer pr = parent.getRegister(name, false);
+            Integer pr = parent.getSymbol(name, false);
             if (pr != null) {
                 if (hoistedVariables == null) {
                     hoistedVariables = new LinkedHashMap<Integer, Integer>();
@@ -131,7 +131,7 @@ public final class Scope {
     /**
      * Declares a parameter.
      * <p>
-     * This method creates an new entry in the named register map.
+     * This method creates an new entry in the symbol map.
      * </p>
      * @param name the parameter name
      */
@@ -152,7 +152,7 @@ public final class Scope {
     /**
      * Declares a local variable.
      * <p>
-     * This method creates an new entry in the named register map.
+     * This method creates an new entry in the symbol map.
      * </p>
      * @param name the variable name
      * @return the register index storing this variable
@@ -202,15 +202,15 @@ public final class Scope {
     }
 
     /**
-     * Gets this script stacked, i.e. parameters and local variables.
-     * @return the register names
+     * Gets this script symbols names, i.e. parameters and local variables.
+     * @return the symbol names
      */
-    public String[] getRegisters() {
+    public String[] getSymbols() {
         return namedVariables != null ? namedVariables.keySet().toArray(new String[0]) : new String[0];
     }
 
     /**
-     * Gets this script parameters, i.e. stacked assigned before creating local variables.
+     * Gets this script parameters, i.e. symbols assigned before creating local variables.
      * @return the parameter names
      */
     public String[] getParameters() {
@@ -229,8 +229,8 @@ public final class Scope {
     }
 
     /**
-     * Gets this script local variable, i.e. stacked assigned to local variables.
-     * @return the parameter names
+     * Gets this script local variable, i.e. symbols assigned to local variables.
+     * @return the local variable names
      */
     public String[] getLocalVariables() {
         if (namedVariables != null && vars > 0) {

Modified: commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java
URL: http://svn.apache.org/viewvc/commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java?rev=1300178&r1=1300177&r2=1300178&view=diff
==============================================================================
--- commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java (original)
+++ commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java Tue Mar 13 15:13:08 2012
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-      
+
 package org.apache.commons.jexl3.internal.introspection;
 
 import java.lang.reflect.Constructor;
@@ -42,8 +42,7 @@ public class Permissions {
     }
 
     /**
-     * Checks whether a constructor
-     * explicitly disallows JEXL introspection.
+     * Checks whether a constructor explicitly disallows JEXL introspection.
      * @param ctor the constructor to check
      * @return true if JEXL is allowed to introspect, false otherwise
      */
@@ -64,8 +63,7 @@ public class Permissions {
     }
 
     /**
-     * Checks whether a field
-     * explicitly disallows JEXL introspection.
+     * Checks whether a field explicitly disallows JEXL introspection.
      * @param field the field to check
      * @return true if JEXL is allowed to introspect, false otherwise
      */

Modified: commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifier.java
URL: http://svn.apache.org/viewvc/commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifier.java?rev=1300178&r1=1300177&r2=1300178&view=diff
==============================================================================
--- commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifier.java (original)
+++ commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifier.java Tue Mar 13 15:13:08 2012
@@ -17,7 +17,7 @@
 package org.apache.commons.jexl3.parser;
 
 /**
- * Identifiers, variables and registers.
+ * Identifiers, variables, ie symbols.
  */
 public class ASTIdentifier extends JexlNode {
     private int register = -1;
@@ -28,18 +28,18 @@ public class ASTIdentifier extends JexlN
     public ASTIdentifier(Parser p, int id) {
         super(p, id);
     }
-    
-    void setRegister(String r) {
+
+    void setSymbol(String r) {
         if (r.charAt(0) == '#') {
             register = Integer.parseInt(r.substring(1));
         }
     }
-    
-    void setRegister(int r) {
+
+    void setSymbol(int r) {
         register = r;
     }
-    
-    public int getRegister() {
+
+    public int getSymbol() {
         return register;
     }
 

Copied: commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccess.java (from r1243213, commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifier.java)
URL: http://svn.apache.org/viewvc/commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccess.java?p2=commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccess.java&p1=commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifier.java&r1=1243213&r2=1300178&rev=1300178&view=diff
==============================================================================
--- commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifier.java (original)
+++ commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccess.java Tue Mar 13 15:13:08 2012
@@ -19,32 +19,32 @@ package org.apache.commons.jexl3.parser;
 /**
  * Identifiers, variables and registers.
  */
-public class ASTIdentifier extends JexlNode {
-    private int register = -1;
-    public ASTIdentifier(int id) {
+public class ASTIdentifierAccess extends JexlNode {
+    private Integer identifier = null;
+    
+    public ASTIdentifierAccess(int id) {
         super(id);
     }
 
-    public ASTIdentifier(Parser p, int id) {
+    public ASTIdentifierAccess(Parser p, int id) {
         super(p, id);
     }
-    
-    void setRegister(String r) {
-        if (r.charAt(0) == '#') {
-            register = Integer.parseInt(r.substring(1));
-        }
-    }
-    
-    void setRegister(int r) {
-        register = r;
-    }
-    
-    public int getRegister() {
-        return register;
-    }
 
     @Override
     public Object jjtAccept(ParserVisitor visitor, Object data) {
         return visitor.visit(this, data);
     }
+    
+    public void setIdentifier(String id) {
+        image = id;
+        try {
+            identifier = Integer.valueOf(id);
+        } catch(NumberFormatException xnumber) {
+            identifier = null;
+        }
+    }
+    
+    public Object getIdentifier() {
+        return identifier != null? identifier : image;
+    }
 }

Modified: commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTJexlScript.java
URL: http://svn.apache.org/viewvc/commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTJexlScript.java?rev=1300178&r1=1300177&r2=1300178&view=diff
==============================================================================
--- commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTJexlScript.java (original)
+++ commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTJexlScript.java Tue Mar 13 15:13:08 2012
@@ -39,20 +39,20 @@ public class ASTJexlScript extends JexlN
     }
 
     /**
-     * Sets the parameters and registers
+     * Sets this script scope.
      * @param theScope the scope
      */
     public void setScope(Scope theScope) {
         this.scope = theScope;
     }
-    
+
     /**
      * Gets this script scope.
      */
     public Scope getScope() {
         return scope;
     }
-    
+
     /**
      * Creates an array of arguments by copying values up to the number of parameters.
      * @param values the argument values
@@ -64,10 +64,10 @@ public class ASTJexlScript extends JexlN
             if (frame != null) {
                 return frame.assign(values);
             }
-        } 
+        }
         return null;
     }
-    
+
     /**
      * Gets the (maximum) number of arguments this script expects.
      * @return the number of parameters
@@ -75,17 +75,17 @@ public class ASTJexlScript extends JexlN
     public int getArgCount() {
         return scope != null? scope.getArgCount() : 0;
     }
-    
+
     /**
-     * Gets this script registers, i.e. parameters and local variables.
-     * @return the register names
+     * Gets this script symbols, i.e. parameters and local variables.
+     * @return the symbol names
      */
-    public String[] getRegisters() {
-        return scope != null? scope.getRegisters() : null;
+    public String[] getSymbols() {
+        return scope != null? scope.getSymbols() : null;
     }
 
     /**
-     * Gets this script parameters, i.e. registers assigned before creating local variables.
+     * Gets this script parameters, i.e. symbols assigned before creating local variables.
      * @return the parameter names
      */
     public String[] getParameters() {
@@ -93,8 +93,8 @@ public class ASTJexlScript extends JexlN
     }
 
     /**
-     * Gets this script local variable, i.e. registers assigned to local variables.
-     * @return the parameter names
+     * Gets this script local variable, i.e. symbols assigned to local variables.
+     * @return the local variable names
      */
     public String[] getLocalVariables() {
         return scope != null? scope.getLocalVariables() : null;

Modified: commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTReferenceExpression.java
URL: http://svn.apache.org/viewvc/commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTReferenceExpression.java?rev=1300178&r1=1300177&r2=1300178&view=diff
==============================================================================
--- commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTReferenceExpression.java (original)
+++ commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/ASTReferenceExpression.java Tue Mar 13 15:13:08 2012
@@ -16,7 +16,7 @@
  */
 package org.apache.commons.jexl3.parser;
 
-public final class ASTReferenceExpression extends ASTArrayAccess {
+public final class ASTReferenceExpression extends JexlNode {
     public ASTReferenceExpression(int id) {
         super(id);
     }

Modified: commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java
URL: http://svn.apache.org/viewvc/commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java?rev=1300178&r1=1300177&r2=1300178&view=diff
==============================================================================
--- commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java (original)
+++ commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java Tue Mar 13 15:13:08 2012
@@ -24,13 +24,17 @@ import org.apache.commons.jexl3.JexlInfo
  * @since 2.0
  */
 public abstract class JexlNode extends SimpleNode {
-    /** A marker interface for literals.
+    /**
+     * A marker interface for literals.
+     *
      * @param <T> the literal type
      */
     public interface Literal<T> {
         T getLiteral();
     }
-    /** token value. */
+    /**
+     * token value.
+     */
     public String image;
 
     public JexlNode(int id) {
@@ -43,6 +47,7 @@ public abstract class JexlNode extends S
 
     /**
      * Gets the associated JexlInfo instance.
+     *
      * @return the info
      */
     public JexlInfo jexlInfo() {
@@ -56,9 +61,19 @@ public abstract class JexlNode extends S
         return null;
     }
 
+    @Override
+    public String toString() {
+        if (image != null) {
+            return super.toString() + ":{" + image + "}";
+        } else {
+            return super.toString();
+        }
+    }
+
     /**
-     * Whether this node is a constant node
-     * Its value can not change after the first evaluation and can be cached indefinitely.
+     * Whether this node is a constant node Its value can not change after the first evaluation and can be cached
+     * indefinitely.
+     *
      * @return true if constant, false otherwise
      */
     public final boolean isConstant() {
@@ -88,4 +103,23 @@ public abstract class JexlNode extends S
         }
         return false;
     }
+
+    /**
+     * Whether this node is a left value.
+     * @return true if node is assignable, false otherwise
+     */
+    public boolean isLeftValue() {
+        if (this instanceof ASTIdentifier || this instanceof ASTIdentifierAccess) {
+            return true;
+        }
+        int nc = this.jjtGetNumChildren() - 1;
+        if (nc >= 0) {
+           JexlNode last = this.jjtGetChild(this.jjtGetNumChildren() - 1);
+           return last.isLeftValue();
+        }
+        if (parent instanceof ASTReference || parent instanceof ASTArrayAccess) {
+            return true;
+        }
+        return false;
+    }
 }