JEXL: 
JEXL-207: split Interpreter in 2 (class was too big), reworked error/exception handling to be more coherent, added tests

git-svn-id: https://svn.apache.org/repos/asf/commons/proper/jexl/trunk@1754746 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/main/java/org/apache/commons/jexl3/JexlException.java b/src/main/java/org/apache/commons/jexl3/JexlException.java
index 3581d8c..d8cd92d 100644
--- a/src/main/java/org/apache/commons/jexl3/JexlException.java
+++ b/src/main/java/org/apache/commons/jexl3/JexlException.java
@@ -416,16 +416,6 @@
         /**
          * Creates a new Property exception instance.
          *
-         * @param node the offending ASTnode
-         * @param var  the unknown variable
-         */
-        public Property(JexlNode node, String var) {
-            this(node, var, null);
-        }
-
-        /**
-         * Creates a new Property exception instance.
-         *
          * @param node  the offending ASTnode
          * @param var   the unknown variable
          * @param cause the exception causing the error
diff --git a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java
index cb93d61..3c2d03c 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java
@@ -23,11 +23,12 @@
 import org.apache.commons.jexl3.JexlException;
 import org.apache.commons.jexl3.JexlOperator;
 import org.apache.commons.jexl3.JexlScript;
+
 import org.apache.commons.jexl3.introspection.JexlMethod;
 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.introspection.JexlUberspect.PropertyResolver;
+
 import org.apache.commons.jexl3.parser.ASTAddNode;
 import org.apache.commons.jexl3.parser.ASTAndNode;
 import org.apache.commons.jexl3.parser.ASTAnnotatedStatement;
@@ -102,7 +103,6 @@
 import org.apache.commons.jexl3.parser.ASTWhileStatement;
 import org.apache.commons.jexl3.parser.JexlNode;
 import org.apache.commons.jexl3.parser.Node;
-import org.apache.commons.jexl3.parser.ParserVisitor;
 
 import java.util.HashMap;
 import java.util.Iterator;
@@ -110,40 +110,25 @@
 import java.util.Map;
 import java.util.concurrent.Callable;
 
-import org.apache.commons.logging.Log;
 
 /**
  * An interpreter of JEXL syntax.
  *
  * @since 2.0
  */
-public class Interpreter extends ParserVisitor {
-    /** The JEXL engine. */
-    protected final Engine jexl;
-    /** The logger. */
-    protected final Log logger;
-    /** The uberspect. */
-    protected final JexlUberspect uberspect;
-    /** The arithmetic handler. */
-    protected final JexlArithmetic arithmetic;
+public class Interpreter extends InterpreterBase {
     /** The operators evaluation delegate. */
     protected final Operators operators;
-    /** The map of symboled functions. */
-    protected final Map<String, Object> functions;
-    /** The map of symboled functions. */
-    protected Map<String, Object> functors;
-    /** The context to store/retrieve variables. */
-    protected final JexlContext context;
-    /** symbol values. */
+    /** Cache executors. */
+    protected final boolean cache;
+    /** Symbol values. */
     protected final Scope.Frame frame;
     /** The context to store/retrieve variables. */
     protected final JexlContext.NamespaceResolver ns;
-    /** Cache executors. */
-    protected final boolean cache;
-    /** Cancellation support. */
-    protected volatile boolean cancelled = false;
-    /** Empty parameters for method matching. */
-    protected static final Object[] EMPTY_PARAMS = new Object[0];
+    /** The map of 'prefix:function' to object resolving as namespaces. */
+    protected final Map<String, Object> functions;
+    /** The map of dynamically creates namespaces, NamespaceFunctor or duck-types of those. */
+    protected Map<String, Object> functors;
 
     /**
      * Creates an interpreter.
@@ -152,32 +137,17 @@
      * @param eFrame   the interpreter evaluation frame
      */
     protected Interpreter(Engine engine, JexlContext aContext, Scope.Frame eFrame) {
-        this.jexl = engine;
-        this.logger = jexl.logger;
-        this.uberspect = jexl.uberspect;
-        this.context = aContext != null ? aContext : Engine.EMPTY_CONTEXT;
-        if (this.context instanceof JexlEngine.Options) {
-            JexlEngine.Options opts = (JexlEngine.Options) context;
-            this.arithmetic = jexl.arithmetic.options(opts);
-            if (!arithmetic.getClass().equals(jexl.arithmetic.getClass())) {
-                logger.error(
-                        "expected arithmetic to be " + jexl.arithmetic.getClass().getSimpleName() +
-                        ", got " + arithmetic.getClass().getSimpleName()
-                );
-            }
-        } else {
-            this.arithmetic = jexl.arithmetic;
-        }
+        super(engine, aContext);
+        this.operators = new Operators(this);
+        this.cache = jexl.cache != null;
+        this.frame = eFrame;
         if (this.context instanceof JexlContext.NamespaceResolver) {
             ns = ((JexlContext.NamespaceResolver) context);
         } else {
             ns = Engine.EMPTY_NS;
         }
         this.functions = jexl.functions;
-        this.cache = jexl.cache != null;
-        this.frame = eFrame;
         this.functors = null;
-        this.operators = new Operators(this);
     }
 
     /**
@@ -212,12 +182,17 @@
             }
             logger.warn(xjexl.getMessage(), xjexl.getCause());
         } finally {
-            if (functors != null && AUTOCLOSEABLE != null) {
-                for (Object functor : functors.values()) {
-                    closeIfSupported(functor);
+            synchronized(this) {
+                if (functors != null) {
+                    if (AUTOCLOSEABLE != null) {
+                        for (Object functor : functors.values()) {
+                            closeIfSupported(functor);
+                        }
+                    }
+                    functors.clear();
+                    functors = null;
                 }
             }
-            functors = null;
             if (context instanceof JexlContext.ThreadLocal) {
                 jexl.putThreadLocal(local);
             }
@@ -225,249 +200,10 @@
         return null;
     }
 
-    /** Java7 AutoCloseable interface defined?. */
-    private static final Class<?> AUTOCLOSEABLE;
-    static {
-        Class<?> c;
-        try {
-            c = Class.forName("java.lang.AutoCloseable");
-        } catch (ClassNotFoundException xclass) {
-            c = null;
-        }
-        AUTOCLOSEABLE = c;
-    }
-
-    /**
-     * Attempt to call close() if supported.
-     * <p>This is used when dealing with auto-closeable (duck-like) objects
-     * @param closeable the object we'd like to close
-     */
-    protected void closeIfSupported(Object closeable) {
-        if (closeable != null) {
-            //if (AUTOCLOSEABLE == null || AUTOCLOSEABLE.isAssignableFrom(closeable.getClass())) {
-            JexlMethod mclose = uberspect.getMethod(closeable, "close", EMPTY_PARAMS);
-            if (mclose != null) {
-                try {
-                    mclose.invoke(closeable, EMPTY_PARAMS);
-                } catch (Exception xignore) {
-                    logger.warn(xignore);
-                }
-            }
-            //}
-        }
-    }
-
-    /**
-     * Whether this interpreter is currently evaluating with a strict engine flag.
-     * @return true if strict engine, false otherwise
-     */
-    protected boolean isStrictEngine() {
-        if (this.context instanceof JexlEngine.Options) {
-            JexlEngine.Options opts = (JexlEngine.Options) context;
-            Boolean strict = opts.isStrict();
-            if (strict != null) {
-                return strict.booleanValue();
-            }
-        }
-        return jexl.isStrict();
-    }
-
-    /**
-     * Whether this interpreter is currently evaluating with a silent mode.
-     * @return true if silent, false otherwise
-     */
-    protected boolean isSilent() {
-        if (this.context instanceof JexlEngine.Options) {
-            JexlEngine.Options opts = (JexlEngine.Options) context;
-            Boolean silent = opts.isSilent();
-            if (silent != null) {
-                return silent.booleanValue();
-            }
-        }
-        return jexl.isSilent();
-    }
-
-    /** @return true if interrupt throws a JexlException.Cancel. */
-    protected boolean isCancellable() {
-        if (this.context instanceof JexlEngine.Options) {
-            JexlEngine.Options opts = (JexlEngine.Options) context;
-            Boolean ocancellable = opts.isCancellable();
-            if (ocancellable != null) {
-                return ocancellable.booleanValue();
-            }
-        }
-        return jexl.cancellable;
-    }
-
-    /**
-     * Finds the node causing a NPE for diadic operators.
-     * @param xrt   the RuntimeException
-     * @param node  the parent node
-     * @param left  the left argument
-     * @param right the right argument
-     * @return the left, right or parent node
-     */
-    protected JexlNode findNullOperand(RuntimeException xrt, JexlNode node, Object left, Object right) {
-        if (xrt instanceof JexlArithmetic.NullOperand) {
-            if (left == null) {
-                return node.jjtGetChild(0);
-            }
-            if (right == null) {
-                return node.jjtGetChild(1);
-            }
-        }
-        return node;
-    }
-
-    /**
-     * Triggered when a variable can not be resolved.
-     * @param node  the node where the error originated from
-     * @param var   the variable name
-     * @param undef whether the variable is undefined or null
-     * @return throws JexlException if isStrict, null otherwise
-     */
-    protected Object unsolvableVariable(JexlNode node, String var, boolean undef) {
-        if (!isSilent()) {
-            logger.warn(JexlException.variableError(node, var, undef));
-        }
-        if (isStrictEngine() && (undef || arithmetic.isStrict())) {
-            throw new JexlException.Variable(node, var, undef);
-        }
-        return null;
-    }
-
-    /**
-     * Triggered when a method can not be resolved.
-     * @param node   the node where the error originated from
-     * @param method the method name
-     * @return throws JexlException if isStrict, null otherwise
-     */
-    protected Object unsolvableMethod(JexlNode node, String method) {
-        if (!isSilent()) {
-            logger.warn(JexlException.methodError(node, method));
-        }
-        if (isStrictEngine()) {
-            throw new JexlException.Method(node, method);
-        }
-        return null;
-    }
-
-    /**
-     * Triggered when a property can not be resolved.
-     * @param node  the node where the error originated from
-     * @param var   the property name
-     * @param cause the cause if any
-     * @return throws JexlException if isStrict, null otherwise
-     */
-    protected Object unsolvableProperty(JexlNode node, String var, Throwable cause) {
-        if (!isSilent()) {
-            logger.warn(JexlException.propertyError(node, var), cause);
-        }
-        if (isStrictEngine()) {
-            throw new JexlException.Property(node, var, cause);
-        }
-        return null;
-    }
-
-    /**
-     * Triggered when an operator fails.
-     * @param node     the node where the error originated from
-     * @param operator the method name
-     * @param cause    the cause of error (if any)
-     * @throws JexlException if isStrict
-     */
-    protected void operatorError(JexlNode node, JexlOperator operator, Throwable cause) {
-        if (cause != null) {
-            if (!isSilent()) {
-                logger.warn(JexlException.operatorError(node, operator.getOperatorSymbol()), cause);
-            }
-            if (isStrictEngine()) {
-                throw new JexlException.Operator(node, operator.getOperatorSymbol(), cause);
-            }
-        }
-    }
-
-    /**
-     * Triggered when method, function or constructor invocation fails.
-     * @param xjexl the JexlException wrapping the original error
-     * @return throws JexlException if isStrict, null otherwise
-     */
-    protected Object invocationFailed(JexlException xjexl) {
-        if (!isSilent()) {
-            logger.warn(xjexl.getMessage(), xjexl.getCause());
-        }
-        if (isStrictEngine()
-                || xjexl instanceof JexlException.Return
-                || xjexl instanceof JexlException.Cancel) {
-            throw xjexl;
-        }
-        return null;
-    }
-
-    /**
-     * Wraps an exception thrown by an invocation.
-     * @param node       the node triggering the exception
-     * @param methodName the method/function name
-     * @param xany       the cause
-     * @return a JexlException
-     */
-    protected JexlException invocationException(JexlNode node, String methodName, Exception xany) {
-        Throwable cause = xany.getCause();
-        if (cause instanceof JexlException) {
-            throw (JexlException) cause;
-        }
-        if (cause instanceof InterruptedException) {
-            cancelled = true;
-            return new JexlException.Cancel(node);
-        }
-        return new JexlException(node, methodName, xany);
-    }
-
-    /**
-     * Triggered when an annotation processing fails.
-     * @param node     the node where the error originated from
-     * @param annotation the annotation name
-     * @param cause    the cause of error (if any)
-     * @throws JexlException if isStrict
-     */
-    protected void annotationError(JexlNode node, String annotation, Throwable cause) {
-        if (!isSilent()) {
-            logger.warn(JexlException.annotationError(node, annotation), cause);
-        }
-        if (isStrictEngine()) {
-            throw new JexlException.Annotation(node, annotation, cause);
-        }
-    }
-
-    /**
-     * Cancels this evaluation, setting the cancel flag that will result in a JexlException.Cancel to be thrown.
-     * @return false if already cancelled, true otherwise
-     */
-    protected boolean cancel() {
-        if (cancelled) {
-            return false;
-        } else {
-            cancelled = true;
-            return true;
-        }
-    }
-
-    /**
-     * Checks whether this interpreter execution was canceled due to thread interruption.
-     * @return true if canceled, false otherwise
-     */
-    protected boolean isCancelled() {
-        if (!cancelled) {
-            cancelled = Thread.currentThread().isInterrupted();
-        }
-        return cancelled;
-    }
-
     /**
      * 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>
+     * 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
@@ -475,10 +211,12 @@
     protected Object resolveNamespace(String prefix, JexlNode node) {
         Object namespace;
         // check whether this namespace is a functor
-        if (functors != null) {
-            namespace = functors.get(prefix);
-            if (namespace != null) {
-                return namespace;
+        synchronized (this) {
+            if (functors != null) {
+                namespace = functors.get(prefix);
+                if (namespace != null) {
+                    return namespace;
+                }
             }
         }
         // check if namespace is a resolver
@@ -506,10 +244,12 @@
         }
         // got a functor, store it and return it
         if (functor != null) {
-            if (functors == null) {
-                functors = new HashMap<String, Object>();
+            synchronized (this) {
+                if (functors == null) {
+                    functors = new HashMap<String, Object>();
+                }
+                functors.put(prefix, functor);
             }
-            functors.put(prefix, functor);
             return functor;
         } else {
             return namespace;
@@ -875,7 +615,7 @@
             // get an iterator for the collection/array etc via the introspector.
             Object forEach = null;
             try {
-                forEach = operators.tryForeachOverload(node, iterableValue);
+                forEach = operators.tryOverload(node, JexlOperator.FOR_EACH, iterableValue);
                 Iterator<?> itemsIterator = forEach instanceof Iterator
                                             ? (Iterator<?>) forEach
                                             : uberspect.getIterator(iterableValue);
@@ -1257,6 +997,7 @@
         JexlNode objectNode;
         StringBuilder ant = null;
         boolean antish = !(parent instanceof ASTReference);
+        boolean pty = true;
         int v = 1;
         main:
         for (int c = 0; c < numChildren; c++) {
@@ -1279,9 +1020,14 @@
             } else if (antish) {  // if we still have a null object, check for an antish variable
                 if (ant == null) {
                     JexlNode first = node.jjtGetChild(0);
-                    if (first instanceof ASTIdentifier && ((ASTIdentifier) first).getSymbol() < 0) {
-                        ant = new StringBuilder(((ASTIdentifier) first).getName());
+                    if (first instanceof ASTIdentifier) {
+                        if (((ASTIdentifier) first).getSymbol() < 0) {
+                            ant = new StringBuilder(((ASTIdentifier) first).getName());
+                        } else {
+                            break;
+                        }
                     } else {
+                        pty = false;
                         break;
                     }
                 }
@@ -1299,10 +1045,14 @@
                 break;
             }
         }
-        if (object == null && antish && ant != null && !isTernaryProtected(node)) {
-            boolean undefined = !(context.has(ant.toString()) || isLocalVariable(node, 0));
-            // variable unknown in context and not a local
-            return unsolvableVariable(node, ant.toString(), undefined);
+        if (object == null && !isTernaryProtected(node)) {
+            if (antish && ant != null) {
+                boolean undefined = !(context.has(ant.toString()) || isLocalVariable(node, 0));
+                // variable unknown in context and not a local
+                return unsolvableVariable(node, ant.toString(), undefined);
+            } else if (!pty) {
+                return unsolvableProperty(node, "<null>.<?>", null);
+            }
         }
         return object;
     }
@@ -1495,11 +1245,11 @@
         }
         if (property == null) {
             // no property, we fail
-            throw new JexlException(propertyNode, "property is null");
+            return unsolvableProperty(propertyNode, "<?>.<null>", null);
         }
         if (object == null) {
             // no object, we fail
-            throw new JexlException(objectNode, "bean is null");
+            return unsolvableProperty(objectNode, "<null>.<?>", null);
         }
         // 3: one before last, assign
         if (assignop != null) {
@@ -1536,7 +1286,7 @@
             object = data;
             if (object == null) {
                 // no object, we fail
-                throw new JexlException(objectNode, "object is null");
+                return unsolvableMethod(objectNode, "<null>.<?>(...)");
             }
         } else {
             method = methodNode.jjtAccept(this, null);
@@ -1545,7 +1295,7 @@
         for (int a = 1; a < node.jjtGetNumChildren(); ++a) {
             if (result == null) {
                 // no method, we fail
-                throw new JexlException(methodNode, "method is null");
+                return unsolvableMethod(methodNode, "<?>.<null>(...)");
             }
             ASTArguments argNode = (ASTArguments) node.jjtGetChild(a);
             result = call(node, object, result, argNode);
@@ -1849,10 +1599,10 @@
                 return eval;
             }
             return unsolvableMethod(node, methodName);
-        } catch (JexlException.Method xmethod) {
-            throw xmethod;
+        } catch (JexlException xthru) {
+            throw xthru;
         } catch (Exception xany) {
-            xjexl = invocationException(node, methodName, xany);
+            xjexl = exceptionOnInvocation(node, methodName, xany);
         }
         return invocationFailed(xjexl);
     }
@@ -1901,11 +1651,11 @@
                 node.jjtSetValue(ctor);
             }
             return instance;
-        } catch (JexlException.Method xmethod) {
-            throw xmethod;
+        } catch (JexlException xthru) {
+            throw xthru;
         } catch (Exception xany) {
             String dbgStr = cobject != null ? cobject.toString() : null;
-            xjexl = invocationException(node, dbgStr, xany);
+            xjexl = exceptionOnInvocation(node, dbgStr, xany);
         }
         return invocationFailed(xjexl);
     }
@@ -2117,15 +1867,15 @@
             Object result = processAnnotation(aname, argv, jstmt);
             // not processing an annotation is an error
             if (!processed[0]) {
-                annotationError(anode, aname, null);
+                return annotationError(anode, aname, null);
+            } else {
+                return result;
             }
-            return result;
         } catch(JexlException xjexl) {
             throw xjexl;
         } catch(Exception xany) {
-            annotationError(anode, aname, xany);
+            return annotationError(anode, aname, xany);
         }
-        return null;
     }
 
     /**
diff --git a/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java
new file mode 100644
index 0000000..5a06090
--- /dev/null
+++ b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java
@@ -0,0 +1,333 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.jexl3.internal;
+
+
+import org.apache.commons.jexl3.JexlArithmetic;
+import org.apache.commons.jexl3.JexlContext;
+import org.apache.commons.jexl3.JexlEngine;
+import org.apache.commons.jexl3.JexlException;
+import org.apache.commons.jexl3.JexlOperator;
+import org.apache.commons.jexl3.introspection.JexlMethod;
+import org.apache.commons.jexl3.introspection.JexlUberspect;
+import org.apache.commons.jexl3.parser.JexlNode;
+import org.apache.commons.jexl3.parser.ParserVisitor;
+
+
+import org.apache.commons.logging.Log;
+
+/**
+ * The helper base of an interpreter of JEXL syntax.
+ * @since 3.0
+ */
+public abstract class InterpreterBase extends ParserVisitor {
+    /** The JEXL engine. */
+    protected final Engine jexl;
+    /** The logger. */
+    protected final Log logger;
+    /** The uberspect. */
+    protected final JexlUberspect uberspect;
+    /** The arithmetic handler. */
+    protected final JexlArithmetic arithmetic;
+    /** The context to store/retrieve variables. */
+    protected final JexlContext context;
+    /** Cancellation support. */
+    protected volatile boolean cancelled = false;
+    /** Empty parameters for method matching. */
+    protected static final Object[] EMPTY_PARAMS = new Object[0];
+
+    /**
+     * Creates an interpreter base.
+     * @param engine   the engine creating this interpreter
+     * @param aContext the context to evaluate expression
+     */
+    protected InterpreterBase(Engine engine, JexlContext aContext) {
+        this.jexl = engine;
+        this.logger = jexl.logger;
+        this.uberspect = jexl.uberspect;
+        this.context = aContext != null ? aContext : Engine.EMPTY_CONTEXT;
+        if (this.context instanceof JexlEngine.Options) {
+            JexlEngine.Options opts = (JexlEngine.Options) context;
+            this.arithmetic = jexl.arithmetic.options(opts);
+            if (!arithmetic.getClass().equals(jexl.arithmetic.getClass())) {
+                logger.error("expected arithmetic to be " + jexl.arithmetic.getClass().getSimpleName()
+                              + ", got " + arithmetic.getClass().getSimpleName()
+                );
+            }
+        } else {
+            this.arithmetic = jexl.arithmetic;
+        }
+    }
+
+    /** Java7 AutoCloseable interface defined?. */
+    protected static final Class<?> AUTOCLOSEABLE;
+    static {
+        Class<?> c;
+        try {
+            c = Class.forName("java.lang.AutoCloseable");
+        } catch (ClassNotFoundException xclass) {
+            c = null;
+        }
+        AUTOCLOSEABLE = c;
+    }
+
+    /**
+     * Attempt to call close() if supported.
+     * <p>This is used when dealing with auto-closeable (duck-like) objects
+     * @param closeable the object we'd like to close
+     */
+    protected void closeIfSupported(Object closeable) {
+        if (closeable != null) {
+            //if (AUTOCLOSEABLE == null || AUTOCLOSEABLE.isAssignableFrom(closeable.getClass())) {
+            JexlMethod mclose = uberspect.getMethod(closeable, "close", EMPTY_PARAMS);
+            if (mclose != null) {
+                try {
+                    mclose.invoke(closeable, EMPTY_PARAMS);
+                } catch (Exception xignore) {
+                    logger.warn(xignore);
+                }
+            }
+            //}
+        }
+    }
+
+    /**
+     * Whether this interpreter is currently evaluating with a strict engine flag.
+     * @return true if strict engine, false otherwise
+     */
+    protected boolean isStrictEngine() {
+        if (this.context instanceof JexlEngine.Options) {
+            JexlEngine.Options opts = (JexlEngine.Options) context;
+            Boolean strict = opts.isStrict();
+            if (strict != null) {
+                return strict.booleanValue();
+            }
+        }
+        return jexl.isStrict();
+    }
+
+    /**
+     * Whether this interpreter is currently evaluating with a silent mode.
+     * @return true if silent, false otherwise
+     */
+    protected boolean isSilent() {
+        if (this.context instanceof JexlEngine.Options) {
+            JexlEngine.Options opts = (JexlEngine.Options) context;
+            Boolean silent = opts.isSilent();
+            if (silent != null) {
+                return silent.booleanValue();
+            }
+        }
+        return jexl.isSilent();
+    }
+
+    /** @return true if interrupt throws a JexlException.Cancel. */
+    protected boolean isCancellable() {
+        if (this.context instanceof JexlEngine.Options) {
+            JexlEngine.Options opts = (JexlEngine.Options) context;
+            Boolean ocancellable = opts.isCancellable();
+            if (ocancellable != null) {
+                return ocancellable.booleanValue();
+            }
+        }
+        return jexl.cancellable;
+    }
+
+    /**
+     * Finds the node causing a NPE for diadic operators.
+     * @param xrt   the RuntimeException
+     * @param node  the parent node
+     * @param left  the left argument
+     * @param right the right argument
+     * @return the left, right or parent node
+     */
+    protected JexlNode findNullOperand(RuntimeException xrt, JexlNode node, Object left, Object right) {
+        if (xrt instanceof JexlArithmetic.NullOperand) {
+            if (left == null) {
+                return node.jjtGetChild(0);
+            }
+            if (right == null) {
+                return node.jjtGetChild(1);
+            }
+        }
+        return node;
+    }
+
+    /**
+     * Triggered when a variable can not be resolved.
+     * @param node  the node where the error originated from
+     * @param var   the variable name
+     * @param undef whether the variable is undefined or null
+     * @return throws JexlException if strict and not silent, null otherwise
+     */
+    protected Object unsolvableVariable(JexlNode node, String var, boolean undef) {
+        if (isStrictEngine()) {
+            if (isSilent()) {
+                logger.warn(JexlException.variableError(node, var, undef));
+            } else if (undef || arithmetic.isStrict()){
+                throw new JexlException.Variable(node, var, undef);
+            }
+        } else if (logger.isDebugEnabled()) {
+            logger.debug(JexlException.variableError(node, var, undef));
+        }
+        return null;
+    }
+
+    /**
+     * Triggered when a method can not be resolved.
+     * @param node   the node where the error originated from
+     * @param method the method name
+     * @return throws JexlException if strict and not silent, null otherwise
+     */
+    protected Object unsolvableMethod(JexlNode node, String method) {
+        if (isStrictEngine()) {
+            if (isSilent()) {
+                logger.warn(JexlException.methodError(node, method));
+            } else {
+                throw new JexlException.Method(node, method);
+            }
+        } else if (logger.isDebugEnabled()) {
+            logger.debug(JexlException.methodError(node, method));
+        }
+        return null;
+    }
+
+    /**
+     * Triggered when a property can not be resolved.
+     * @param node  the node where the error originated from
+     * @param var   the property name
+     * @param cause the cause if any
+     * @return throws JexlException if strict and not silent, null otherwise
+     */
+    protected Object unsolvableProperty(JexlNode node, String var, Throwable cause) {
+        if (isStrictEngine()) {
+            if (isSilent()) {
+                logger.warn(JexlException.propertyError(node, var), cause);
+            } else {
+                throw new JexlException.Property(node, var, cause);
+            }
+        } else if (logger.isDebugEnabled()) {
+            logger.debug(JexlException.propertyError(node, var), cause);
+        }
+        return null;
+    }
+
+    /**
+     * Triggered when an operator fails.
+     * @param node     the node where the error originated from
+     * @param operator the method name
+     * @param cause    the cause of error (if any)
+     * @return throws JexlException if strict and not silent, null otherwise
+     */
+    protected Object operatorError(JexlNode node, JexlOperator operator, Throwable cause) {
+        if (isStrictEngine()) {
+            if (isSilent()) {
+                logger.warn(JexlException.operatorError(node, operator.getOperatorSymbol()), cause);
+            } else {
+                throw new JexlException.Operator(node, operator.getOperatorSymbol(), cause);
+            }
+        } else if (logger.isDebugEnabled()) {
+            logger.debug(JexlException.operatorError(node, operator.getOperatorSymbol()), cause);
+        }
+        return null;
+    }
+
+    /**
+     * Triggered when method, function or constructor invocation fails.
+     * @param xjexl the JexlException wrapping the original error
+     * @return throws a JexlException if strict and not silent, null otherwise
+     */
+    protected Object invocationFailed(JexlException xjexl) {
+        if (xjexl instanceof JexlException.Return
+            || xjexl instanceof JexlException.Cancel) {
+            throw xjexl;
+        }
+        if (isStrictEngine())  {
+            if (isSilent()) {
+                logger.warn(xjexl.getMessage(), xjexl.getCause());
+            } else {
+                throw xjexl;
+            }
+        } else if (logger.isDebugEnabled()) {
+            logger.debug(xjexl);
+        }
+        return null;
+    }
+
+    /**
+     * Wraps an exception thrown by an invocation.
+     * @param node       the node triggering the exception
+     * @param methodName the method/function name
+     * @param xany       the cause
+     * @return a JexlException
+     */
+    protected JexlException exceptionOnInvocation(JexlNode node, String methodName, Exception xany) {
+        Throwable cause = xany.getCause();
+        if (cause instanceof JexlException) {
+            return (JexlException) cause;
+        }
+        if (cause instanceof InterruptedException) {
+            cancelled = true;
+            return new JexlException.Cancel(node);
+        }
+        return new JexlException(node, methodName, xany);
+    }
+
+    /**
+     * Triggered when an annotation processing fails.
+     * @param node     the node where the error originated from
+     * @param annotation the annotation name
+     * @param cause    the cause of error (if any)
+     * @return throws a JexlException if strict and not silent, null otherwise
+     */
+    protected Object annotationError(JexlNode node, String annotation, Throwable cause) {
+        if (isStrictEngine()) {
+            if (isSilent()) {
+                logger.warn(JexlException.annotationError(node, annotation), cause);
+            } else {
+                throw new JexlException.Annotation(node, annotation, cause);
+            }
+        } else if (logger.isDebugEnabled()) {
+            logger.debug(JexlException.annotationError(node, annotation), cause);
+        }
+        return null;
+    }
+
+    /**
+     * Cancels this evaluation, setting the cancel flag that will result in a JexlException.Cancel to be thrown.
+     * @return false if already cancelled, true otherwise
+     */
+    protected synchronized boolean  cancel() {
+        if (cancelled) {
+            return false;
+        } else {
+            cancelled = true;
+            return true;
+        }
+    }
+
+    /**
+     * Checks whether this interpreter execution was canceled due to thread interruption.
+     * @return true if canceled, false otherwise
+     */
+    protected synchronized boolean isCancelled() {
+        if (!cancelled) {
+            cancelled = Thread.currentThread().isInterrupted();
+        }
+        return cancelled;
+    }
+}
diff --git a/src/main/java/org/apache/commons/jexl3/internal/Operators.java b/src/main/java/org/apache/commons/jexl3/internal/Operators.java
index df0b132..ed5fd38 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Operators.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Operators.java
@@ -24,7 +24,6 @@
 import org.apache.commons.jexl3.introspection.JexlUberspect;
 import org.apache.commons.jexl3.parser.JexlNode;
 
-import java.util.Iterator;
 /**
  * Helper class to deal with operator overloading and specifics.
  * @since 3.0
@@ -92,45 +91,7 @@
                     return result;
                 }
             } catch (Exception xany) {
-                interpreter.operatorError(node, operator, xany);
-            }
-        }
-        return JexlEngine.TRY_FAILED;
-    }
-
-    /**
-     * Attempts to call an overloaded forEach operator.
-     * <p>
-     * This takes care of finding and caching the operator method when appropriate
-     * @param node     the syntactic node
-     * @param arg      the argument
-     * @return the result of the operator evaluation or TRY_FAILED
-     */
-    protected Object tryForeachOverload(JexlNode node, Object arg) {
-        if (operators != null && operators.overloads(JexlOperator.FOR_EACH)) {
-            final JexlArithmetic arithmetic = interpreter.arithmetic;
-            final boolean cache = interpreter.cache;
-            if (cache) {
-                Object cached = node.jjtGetValue();
-                if (cached instanceof JexlMethod) {
-                    JexlMethod me = (JexlMethod) cached;
-                    Object eval = me.tryInvoke(JexlOperator.FOR_EACH.getMethodName(), arithmetic, arg);
-                    if (!me.tryFailed(eval)) {
-                        return eval;
-                    }
-                }
-            }
-            try {
-                JexlMethod vm = operators.getOperator(JexlOperator.FOR_EACH, arg);
-                if (vm != null && Iterator.class.isAssignableFrom(vm.getReturnType())) {
-                    Object result = vm.invoke(arithmetic, arg);
-                    if (cache) {
-                        node.jjtSetValue(vm);
-                    }
-                    return result;
-                }
-            } catch (Exception xany) {
-                interpreter.operatorError(node, JexlOperator.FOR_EACH, xany);
+                return interpreter.operatorError(node, operator, xany);
             }
         }
         return JexlEngine.TRY_FAILED;
diff --git a/src/test/java/org/apache/commons/jexl3/AnnotationTest.java b/src/test/java/org/apache/commons/jexl3/AnnotationTest.java
index 1955aa4..5cdd200 100644
--- a/src/test/java/org/apache/commons/jexl3/AnnotationTest.java
+++ b/src/test/java/org/apache/commons/jexl3/AnnotationTest.java
@@ -124,33 +124,59 @@
 
     @Test
     public void testError() throws Exception {
+        testError(true);
+        testError(false);
+    }
+
+    private void testError(boolean silent) throws Exception {
+        CaptureLog log = new CaptureLog();
         AnnotationContext jc = new AnnotationContext();
-        JexlEngine jexl = new JexlBuilder().create();
+        JexlEngine jexl = new JexlBuilder().logger(log).strict(true).silent(silent).create();
         JexlScript e = jexl.createScript("@error('42') { return 42; }");
         try {
             Object r = e.execute(jc);
-            Assert.fail("should have failed");
+            if (!silent) {
+                Assert.fail("should have failed");
+            } else {
+                Assert.assertEquals(1, log.count("warn"));
+            }
         } catch (JexlException.Annotation xjexl) {
             Assert.assertEquals("error", xjexl.getAnnotation());
         }
         Assert.assertEquals(1, jc.getCount());
         Assert.assertTrue(jc.getNames().contains("error"));
         Assert.assertTrue(jc.getNames().contains("42"));
+        if (!silent) {
+            Assert.assertEquals(0, log.count("warn"));
+        }
     }
 
     @Test
     public void testUnknown() throws Exception {
+        testUnknown(true);
+        testUnknown(false);
+    }
+
+    private void testUnknown(boolean silent) throws Exception {
+        CaptureLog log = new CaptureLog();
         AnnotationContext jc = new AnnotationContext();
-        JexlEngine jexl = new JexlBuilder().create();
+        JexlEngine jexl = new JexlBuilder().logger(log).strict(true).silent(silent).create();
         JexlScript e = jexl.createScript("@unknown('42') { return 42; }");
         try {
             Object r = e.execute(jc);
-            Assert.fail("should have failed");
+            if (!silent) {
+                Assert.fail("should have failed");
+            } else {
+                Assert.assertEquals(1, log.count("warn"));
+            }
         } catch (JexlException.Annotation xjexl) {
             Assert.assertEquals("unknown", xjexl.getAnnotation());
         }
         Assert.assertEquals(1, jc.getCount());
         Assert.assertTrue(jc.getNames().contains("unknown"));
         Assert.assertFalse(jc.getNames().contains("42"));
+        if (!silent) {
+            Assert.assertEquals(0, log.count("warn"));
+        }
     }
 }
diff --git a/src/test/java/org/apache/commons/jexl3/ArithmeticOperatorTest.java b/src/test/java/org/apache/commons/jexl3/ArithmeticOperatorTest.java
index ae2b988..88308ac 100644
--- a/src/test/java/org/apache/commons/jexl3/ArithmeticOperatorTest.java
+++ b/src/test/java/org/apache/commons/jexl3/ArithmeticOperatorTest.java
@@ -369,6 +369,10 @@
         public Date now() {
             return new Date(System.currentTimeMillis());
         }
+
+        public Date multiply(Date d0, Date d1) {
+            throw new ArithmeticException("unsupported");
+        }
     }
 
     public static class DateContext extends MapContext {
@@ -389,6 +393,34 @@
     }
 
     @Test
+    public void testOperatorError() throws Exception {
+        testOperatorError(true);
+        testOperatorError(false);
+    }
+
+    private void testOperatorError(boolean silent) throws Exception {
+        CaptureLog log = new CaptureLog();
+        DateContext jc = new DateContext();
+        Date d = new Date();
+        JexlEngine jexl = new JexlBuilder().logger(log).strict(true).silent(silent).cache(32)
+                                           .arithmetic(new DateArithmetic(true)).create();
+        JexlScript expr0 = jexl.createScript("date * date", "date");
+        try {
+            Object value0 = expr0.execute(jc, d);
+            if (!silent) {
+                Assert.fail("should have failed");
+            } else {
+                Assert.assertEquals(1, log.count("warn"));
+            }
+        } catch(JexlException.Operator xop) {
+            Assert.assertEquals("*", xop.getSymbol());
+        }
+        if (!silent) {
+            Assert.assertEquals(0, log.count("warn"));
+        }
+    }
+
+    @Test
     public void testDateArithmetic() throws Exception {
         Date d = new Date();
         JexlContext jc = new MapContext();
diff --git a/src/test/java/org/apache/commons/jexl3/CaptureLog.java b/src/test/java/org/apache/commons/jexl3/CaptureLog.java
new file mode 100644
index 0000000..2ae0aa6
--- /dev/null
+++ b/src/test/java/org/apache/commons/jexl3/CaptureLog.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2016 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.jexl3;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.logging.Log;
+
+/**
+ * A log implementation to help control tests results.
+ */
+public class CaptureLog implements Log {
+    private List<Object[]> captured = new ArrayList<Object[]>();
+
+    static Object caller() {
+        StackTraceElement[] stack = new Exception().fillInStackTrace().getStackTrace();
+        return stack[2];
+    }
+
+    public boolean isEmpty() {
+        return captured.isEmpty();
+    }
+
+    public int count(String type) {
+        int count = 0;
+        for (Object[] l : captured) {
+            if (type.equals(l[0].toString())) {
+                count += 1;
+            }
+        }
+        return count;
+    }
+
+    @Override
+    public void debug(Object o) {
+        captured.add(new Object[]{"debug", caller(), o});
+    }
+
+    @Override
+    public void debug(Object o, Throwable thrwbl) {
+        captured.add(new Object[]{"debug", caller(), o, thrwbl});
+    }
+
+    @Override
+    public void error(Object o) {
+        captured.add(new Object[]{"error", caller(), o});
+    }
+
+    @Override
+    public void error(Object o, Throwable thrwbl) {
+        captured.add(new Object[]{"error", caller(), o, thrwbl});
+    }
+
+    @Override
+    public void fatal(Object o) {
+        captured.add(new Object[]{"fatal", caller(), o});
+    }
+
+    @Override
+    public void fatal(Object o, Throwable thrwbl) {
+        captured.add(new Object[]{"fatal", caller(), o, thrwbl});
+    }
+
+    @Override
+    public void info(Object o) {
+        captured.add(new Object[]{"info", caller(), o});
+    }
+
+    @Override
+    public void info(Object o, Throwable thrwbl) {
+        captured.add(new Object[]{"info", caller(), o, thrwbl});
+    }
+
+    @Override
+    public boolean isDebugEnabled() {
+        return true;
+    }
+
+    @Override
+    public boolean isErrorEnabled() {
+        return true;
+    }
+
+    @Override
+    public boolean isFatalEnabled() {
+        return true;
+    }
+
+    @Override
+    public boolean isInfoEnabled() {
+        return true;
+    }
+
+    @Override
+    public boolean isTraceEnabled() {
+        return true;
+    }
+
+    @Override
+    public boolean isWarnEnabled() {
+        return true;
+    }
+
+    @Override
+    public void trace(Object o) {
+        captured.add(new Object[]{"trace", caller(), o});
+    }
+
+    @Override
+    public void trace(Object o, Throwable thrwbl) {
+        captured.add(new Object[]{"trace", caller(), o, thrwbl});
+    }
+
+    @Override
+    public void warn(Object o) {
+        captured.add(new Object[]{"warn", caller(), o});
+    }
+
+    @Override
+    public void warn(Object o, Throwable thrwbl) {
+        captured.add(new Object[]{"warn", caller(), o, thrwbl});
+    }
+
+}
diff --git a/src/test/java/org/apache/commons/jexl3/ExceptionTest.java b/src/test/java/org/apache/commons/jexl3/ExceptionTest.java
index 003aa13..81f870d 100644
--- a/src/test/java/org/apache/commons/jexl3/ExceptionTest.java
+++ b/src/test/java/org/apache/commons/jexl3/ExceptionTest.java
@@ -16,7 +16,10 @@
  */
 package org.apache.commons.jexl3;
 
+import java.util.ArrayList;
+import java.util.List;
 import org.apache.commons.jexl3.internal.Engine;
+import org.apache.commons.logging.Log;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -160,4 +163,52 @@
             Assert.assertTrue(msg.indexOf("c.e") > 0);
         }
     }
+
+
+    @Test
+    public void test206() throws Exception {
+        String src = "null.1 = 2; return 42";
+        doTest206(src, false, false);
+        doTest206(src, false, true);
+        doTest206(src, true, false);
+        doTest206(src, true, true);
+        src = "x = null.1; return 42";
+        doTest206(src, false, false);
+        doTest206(src, false, true);
+        doTest206(src, true, false);
+        doTest206(src, true, true);
+        src = "x = y.1; return 42";
+        doTest206(src, false, false);
+        doTest206(src, false, true);
+        doTest206(src, true, false);
+        doTest206(src, true, true);
+    }
+    private void doTest206(String src, boolean strict, boolean silent) throws Exception {
+        CaptureLog l = new CaptureLog();
+        JexlContext jc = new MapContext();
+        JexlEngine jexl = new JexlBuilder().logger(l).strict(strict).silent(silent).create();
+        JexlScript e;
+        Object r = -1;
+        e = jexl.createScript(src);
+        try {
+            r = e.execute(jc);
+            if (strict && !silent) {
+                Assert.fail("should have thrown an exception");
+            }
+        } catch(JexlException xjexl) {
+            if (!strict || silent) {
+                Assert.fail("should not have thrown an exception");
+            }
+        }
+        if (strict) {
+            if (silent && l.count("warn") == 0) {
+                Assert.fail("should have generated a warning");
+            }
+        } else {
+            if (l.count("debug") == 0) {
+                Assert.fail("should have generated a debug");
+            }
+            Assert.assertEquals(42, r);
+        }
+    }
 }
diff --git a/src/test/java/org/apache/commons/jexl3/IssuesTest.java b/src/test/java/org/apache/commons/jexl3/IssuesTest.java
index 83eafa2..d139f9c 100644
--- a/src/test/java/org/apache/commons/jexl3/IssuesTest.java
+++ b/src/test/java/org/apache/commons/jexl3/IssuesTest.java
@@ -17,17 +17,19 @@
 package org.apache.commons.jexl3;
 
 import org.apache.commons.jexl3.internal.Engine;
+import org.apache.commons.jexl3.internal.introspection.Uberspect;
+
+import java.io.File;
 import java.math.BigDecimal;
 import java.math.MathContext;
-import java.util.HashMap;
-import java.util.Map;
-import org.apache.commons.jexl3.internal.introspection.Uberspect;
-import java.io.File;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
+
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
diff --git a/src/test/java/org/apache/commons/jexl3/ScriptCallableTest.java b/src/test/java/org/apache/commons/jexl3/ScriptCallableTest.java
index 661c0c1..0e4e34b 100644
--- a/src/test/java/org/apache/commons/jexl3/ScriptCallableTest.java
+++ b/src/test/java/org/apache/commons/jexl3/ScriptCallableTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.commons.jexl3;
 
+import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.ExecutionException;
@@ -27,6 +28,8 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import org.apache.commons.jexl3.internal.Script;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -35,7 +38,7 @@
  */
 @SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"})
 public class ScriptCallableTest extends JexlTestCase {
-    //Logger LOGGER = Logger.getLogger(VarTest.class.getName());
+    //private Log logger = LogFactory.getLog(JexlEngine.class);
     public ScriptCallableTest() {
         super("ScriptCallableTest");
     }
@@ -64,6 +67,7 @@
 
     @Test
     public void testCallableCancel() throws Exception {
+        List<Runnable> lr = null;
         final Semaphore latch = new Semaphore(0);
         JexlContext ctxt = new MapContext();
         ctxt.set("latch", latch);
@@ -89,13 +93,15 @@
             // ok, ignore
             Assert.assertTrue(xexec.getCause() instanceof JexlException.Cancel);
         } finally {
-            executor.shutdown();
+            lr = executor.shutdownNow();
         }
         Assert.assertTrue(c.isCancelled());
+        Assert.assertTrue(lr == null || lr.isEmpty());
     }
 
     @Test
     public void testCallableTimeout() throws Exception {
+        List<Runnable> lr = null;
         final Semaphore latch = new Semaphore(0);
         JexlContext ctxt = new MapContext();
         ctxt.set("latch", latch);
@@ -114,14 +120,16 @@
             // ok, ignore
             future.cancel(true);
         } finally {
-            executor.shutdown();
+            lr = executor.shutdownNow();
         }
         Assert.assertTrue(future.isCancelled());
         Assert.assertEquals(42, t);
+        Assert.assertTrue(lr.isEmpty());
     }
 
     @Test
     public void testCallableClosure() throws Exception {
+        List<Runnable> lr = null;
         JexlScript e = JEXL.createScript("function(t) {while(t);}");
         Callable<Object> c = e.callable(null, Boolean.TRUE);
         Object t = 42;
@@ -135,10 +143,11 @@
             // ok, ignore
             future.cancel(true);
         } finally {
-            executor.shutdown();
+            lr = executor.shutdownNow();
         }
         Assert.assertTrue(future.isCancelled());
         Assert.assertEquals(42, t);
+        Assert.assertTrue(lr.isEmpty());
     }
 
     public static class TestContext extends MapContext implements JexlContext.NamespaceResolver {
@@ -191,6 +200,7 @@
 
     @Test
     public void testNoWait() throws Exception {
+        List<Runnable> lr = null;
         JexlScript e = JEXL.createScript("wait(0)");
         Callable<Object> c = e.callable(new TestContext());
 
@@ -201,12 +211,14 @@
             Assert.assertTrue(future.isDone());
             Assert.assertEquals(0, t);
         } finally {
-            executor.shutdown();
+            lr = executor.shutdownNow();
         }
+        Assert.assertTrue(lr.isEmpty());
     }
 
     @Test
     public void testWait() throws Exception {
+        List<Runnable> lr = null;
         JexlScript e = JEXL.createScript("wait(1)");
         Callable<Object> c = e.callable(new TestContext());
 
@@ -216,12 +228,14 @@
             Object t = future.get(2, TimeUnit.SECONDS);
             Assert.assertEquals(1, t);
         } finally {
-            executor.shutdown();
+            lr = executor.shutdownNow();
         }
+        Assert.assertTrue(lr.isEmpty());
     }
 
     @Test
     public void testCancelWait() throws Exception {
+        List<Runnable> lr = null;
         JexlScript e = JEXL.createScript("wait(10)");
         Callable<Object> c = e.callable(new TestContext());
 
@@ -239,12 +253,14 @@
             Assert.assertTrue(future.isCancelled());
             Assert.assertEquals(42, t);
         } finally {
-            executor.shutdown();
+            lr = executor.shutdownNow();
         }
+        Assert.assertTrue(lr.isEmpty());
     }
 
     @Test
     public void testCancelWaitInterrupt() throws Exception {
+        List<Runnable> lr = null;
         JexlScript e = JEXL.createScript("waitInterrupt(42)");
         Callable<Object> c = e.callable(new TestContext());
 
@@ -259,14 +275,16 @@
             // ok, ignore
             future.cancel(true);
         } finally {
-            executor.shutdown();
+            lr = executor.shutdownNow();
         }
         Assert.assertTrue(future.isCancelled());
         Assert.assertEquals(42, t);
+        Assert.assertTrue(lr.isEmpty());
     }
 
     @Test
     public void testCancelForever() throws Exception {
+        List<Runnable> lr = null;
         final Semaphore latch = new Semaphore(0);
         JexlContext ctxt = new TestContext();
         ctxt.set("latch", latch);
@@ -286,14 +304,16 @@
             // ok, ignore
             future.cancel(true);
         } finally {
-            executor.shutdown();
+            lr = executor.shutdownNow();
         }
         Assert.assertTrue(future.isCancelled());
         Assert.assertEquals(42, t);
+        Assert.assertTrue(lr.isEmpty());
     }
 
     @Test
     public void testCancelLoopWait() throws Exception {
+        List<Runnable> lr = null;
         JexlScript e = JEXL.createScript("while (true) { wait(10) }");
         Callable<Object> c = e.callable(new TestContext());
 
@@ -307,10 +327,11 @@
         } catch (TimeoutException xtimeout) {
             future.cancel(true);
         } finally {
-            executor.shutdown();
+            lr = executor.shutdownNow();
         }
         Assert.assertTrue(future.isCancelled());
         Assert.assertEquals(42, t);
+        Assert.assertTrue(lr.isEmpty());
     }
 
     @Test
@@ -345,6 +366,7 @@
      * @throws Exception if there is a regression
      */
     private void runInterrupt(JexlEngine jexl) throws Exception {
+        List<Runnable> lr = null;
         ExecutorService exec = Executors.newFixedThreadPool(2);
         try {
             JexlContext ctxt = new TestContext();
@@ -411,7 +433,7 @@
                     }
                 };
                 exec.submit(cancels);
-                t = fc.get();
+                t = f.get(100L, TimeUnit.MILLISECONDS);
                 Assert.fail("should be cancelled");
             } catch (CancellationException xexec) {
                 // this is the expected result
@@ -452,8 +474,9 @@
             }
             Assert.assertNotEquals(42, t);
         } finally {
-            exec.shutdown();
+            lr = exec.shutdownNow();
         }
+        Assert.assertTrue(lr.isEmpty());
     }
 
     @Test
diff --git a/src/test/java/org/apache/commons/jexl3/ScriptTest.java b/src/test/java/org/apache/commons/jexl3/ScriptTest.java
index 1bdf2c5..a61bbe4 100644
--- a/src/test/java/org/apache/commons/jexl3/ScriptTest.java
+++ b/src/test/java/org/apache/commons/jexl3/ScriptTest.java
@@ -28,6 +28,7 @@
 @SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"})
 public class ScriptTest extends JexlTestCase {
     static final String TEST1 =  "src/test/scripts/test1.jexl";
+    static final String TEST_ADD =  "src/test/scripts/testAdd.jexl";
 
     // test class for testScriptUpdatesContext
     // making this class private static will cause the test to fail.
@@ -74,8 +75,18 @@
         Assert.assertEquals("Wrong result", new Integer(7), result);
     }
 
+    @Test public void testArgScriptFromFile() throws Exception {
+        File testScript = new File(TEST_ADD);
+        JexlScript s = JEXL.createScript(testScript,new String[]{"x","y"});
+        JexlContext jc = new MapContext();
+        jc.set("out", System.out);
+        Object result = s.execute(jc, 13, 29);
+        Assert.assertNotNull("No result", result);
+        Assert.assertEquals("Wrong result", new Integer(42), result);
+    }
+
     @Test public void testScriptFromURL() throws Exception {
-        URL testUrl = new File("src/test/scripts/test1.jexl").toURI().toURL();
+        URL testUrl = new File(TEST1).toURI().toURL();
         JexlScript s = JEXL.createScript(testUrl);
         JexlContext jc = new MapContext();
         jc.set("out", System.out);
@@ -84,6 +95,16 @@
         Assert.assertEquals("Wrong result", new Integer(7), result);
     }
 
+    @Test public void testArgScriptFromURL() throws Exception {
+        URL testUrl = new File(TEST_ADD).toURI().toURL();
+        JexlScript s = JEXL.createScript(testUrl,new String[]{"x","y"});
+        JexlContext jc = new MapContext();
+        jc.set("out", System.out);
+        Object result = s.execute(jc, 13, 29);
+        Assert.assertNotNull("No result", result);
+        Assert.assertEquals("Wrong result", new Integer(42), result);
+    }
+
     @Test public void testScriptUpdatesContext() throws Exception {
         String jexlCode = "resultat.setCode('OK')";
         JexlExpression e = JEXL.createExpression(jexlCode);
diff --git a/src/test/resources/log4j.xml b/src/test/resources/log4j.xml
index d0ae637..b46ac17 100644
--- a/src/test/resources/log4j.xml
+++ b/src/test/resources/log4j.xml
@@ -21,7 +21,7 @@
             <param name="ConversionPattern" value="%d{ABSOLUTE} %5p %c{1}:%L - %m%n"/>
         </layout>
     </appender>
-    
+
     <logger name="org.apache.commons.jexl3" additivity="false">
         <level value="error"/>
         <appender-ref ref="Console"/>
diff --git a/src/test/scripts/testAdd.jexl b/src/test/scripts/testAdd.jexl
new file mode 100644
index 0000000..ed97e0c
--- /dev/null
+++ b/src/test/scripts/testAdd.jexl
@@ -0,0 +1,18 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+//(x, y)->{ x + y }
+x + y