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 2022/03/14 15:31:37 UTC

[commons-jexl] branch master updated: JEXL-364: let evaluation options flow through closures

This is an automated email from the ASF dual-hosted git repository.

henrib pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-jexl.git


The following commit(s) were added to refs/heads/master by this push:
     new 4b3d1ca  JEXL-364: let evaluation options flow through closures
4b3d1ca is described below

commit 4b3d1ca57d64655fa6b669d7a9cbe87f494352ee
Author: henrib <he...@apache.org>
AuthorDate: Mon Mar 14 16:31:30 2022 +0100

    JEXL-364: let evaluation options flow through closures
---
 .../java/org/apache/commons/jexl3/JexlContext.java |  20 +++
 .../java/org/apache/commons/jexl3/JexlOptions.java |  14 +-
 .../org/apache/commons/jexl3/internal/Closure.java |  14 +-
 .../org/apache/commons/jexl3/internal/Engine.java  |   9 +-
 .../apache/commons/jexl3/internal/Interpreter.java |  12 +-
 .../org/apache/commons/jexl3/internal/Script.java  |  22 +++-
 .../commons/jexl3/internal/TemplateEngine.java     |  16 +--
 .../jexl3/internal/TemplateInterpreter.java        |   9 +-
 .../commons/jexl3/internal/TemplateScript.java     |   4 +-
 .../org/apache/commons/jexl3/Issues300Test.java    | 141 +++++++++++++++++++++
 .../java/org/apache/commons/jexl3/JXLTTest.java    |  50 ++------
 .../org/apache/commons/jexl3/JexlTestCase.java     |  39 ++++++
 .../java/org/apache/commons/jexl3/LambdaTest.java  | 138 ++++++++++----------
 .../commons/jexl3/internal/OptionsContext.java     |  38 ++++++
 14 files changed, 389 insertions(+), 137 deletions(-)

diff --git a/src/main/java/org/apache/commons/jexl3/JexlContext.java b/src/main/java/org/apache/commons/jexl3/JexlContext.java
index 658a8f8..67dc118 100644
--- a/src/main/java/org/apache/commons/jexl3/JexlContext.java
+++ b/src/main/java/org/apache/commons/jexl3/JexlContext.java
@@ -172,10 +172,30 @@ public interface JexlContext {
     interface PragmaProcessor {
         /**
          * Process one pragma.
+         * <p>Never called in 3.3, must be implemented for 3.2 binary compatibility reasons.</p>
+         * <p>Typical implementation in 3.3:</p>
+         * <code>
+         *         &#64;Override
+         *         public void processPragma(String key, Object value) {
+         *             processPragma(null, key, value);
+         *         }
+         * </code>
          * @param key the key
          * @param value the value
+         * @deprecated 3.3
          */
         void processPragma(String key, Object value);
+
+        /**
+         * Process one pragma.
+         * @param opts the current evaluator options
+         * @param key the key
+         * @param value the value
+         * @since 3.3
+         */
+        default void processPragma(JexlOptions opts, String key, Object value) {
+            processPragma(key, value);
+        }
     }
 
     /**
diff --git a/src/main/java/org/apache/commons/jexl3/JexlOptions.java b/src/main/java/org/apache/commons/jexl3/JexlOptions.java
index ab9e573..1a37824 100644
--- a/src/main/java/org/apache/commons/jexl3/JexlOptions.java
+++ b/src/main/java/org/apache/commons/jexl3/JexlOptions.java
@@ -157,7 +157,7 @@ public final class JexlOptions {
      * Sets this option flags using the +/- syntax.
      * @param opts the option flags
      */
-    public void setFlags(final String[] opts) {
+    public void setFlags(final String... opts) {
         flags = parseFlags(flags, opts);
     }
 
@@ -417,4 +417,16 @@ public final class JexlOptions {
         return new JexlOptions().set(this);
     }
 
+    @Override public String toString() {
+        StringBuilder strb = new StringBuilder();
+        for(int i = 0; i < NAMES.length; ++i) {
+            if (i > 0) {
+                strb.append(' ');
+            }
+            strb.append((flags & (1 << i)) != 0? '+':'-');
+            strb.append(NAMES[i]);
+        }
+        return strb.toString();
+    }
+
 }
diff --git a/src/main/java/org/apache/commons/jexl3/internal/Closure.java b/src/main/java/org/apache/commons/jexl3/internal/Closure.java
index 18ae8e1..ced91c1 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Closure.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Closure.java
@@ -17,6 +17,7 @@
 package org.apache.commons.jexl3.internal;
 
 import org.apache.commons.jexl3.JexlContext;
+import org.apache.commons.jexl3.JexlOptions;
 import org.apache.commons.jexl3.parser.ASTJexlLambda;
 
 import java.util.Objects;
@@ -27,6 +28,8 @@ import java.util.Objects;
 public class Closure extends Script {
     /** The frame. */
     protected final Frame frame;
+    /** The options. */
+    protected final JexlOptions options;
 
     /**
      * Creates a closure.
@@ -36,6 +39,8 @@ public class Closure extends Script {
     protected Closure(final Interpreter theCaller, final ASTJexlLambda lambda) {
         super(theCaller.jexl, null, lambda);
         frame = lambda.createFrame(theCaller.frame);
+        JexlOptions callerOptions = theCaller.options;
+        options = callerOptions != null ? callerOptions.copy() :  null;
     }
 
     /**
@@ -49,6 +54,11 @@ public class Closure extends Script {
         frame = sf == null
                 ? script.createFrame(args)
                 : sf.assign(args);
+        JexlOptions closureOptions = null;
+        if (base instanceof Closure) {
+            closureOptions = ((Closure) base).options;
+        }
+        options = closureOptions != null ? closureOptions.copy() :  null;
     }
 
     @Override
@@ -123,14 +133,14 @@ public class Closure extends Script {
     @Override
     public Object execute(final JexlContext context, final Object... args) {
         final Frame local = frame != null? frame.assign(args) : null;
-        final Interpreter interpreter = createInterpreter(context, local);
+        final Interpreter interpreter = createInterpreter(context, local, options);
         return interpreter.runClosure(this, null);
     }
 
     @Override
     public Callable callable(final JexlContext context, final Object... args) {
         final Frame local = frame != null? frame.assign(args) : null;
-        return new Callable(createInterpreter(context, local)) {
+        return new Callable(createInterpreter(context, local, options)) {
             @Override
             public Object interpret() {
                 return interpreter.runClosure(Closure.this, null);
diff --git a/src/main/java/org/apache/commons/jexl3/internal/Engine.java b/src/main/java/org/apache/commons/jexl3/internal/Engine.java
index 11d6338..c701b6f 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Engine.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Engine.java
@@ -449,7 +449,7 @@ public class Engine extends JexlEngine {
                     }
                 }
                 if (processor != null) {
-                    processor.processPragma(key, value);
+                    processor.processPragma(opts, key, value);
                 }
             }
             if (ns != null) {
@@ -493,6 +493,13 @@ public class Engine extends JexlEngine {
         return new Interpreter(this, opts, context, frame);
     }
 
+    /**
+     * Creates a template interpreter.
+     * @param args the template interpreter arguments
+     */
+    protected Interpreter createTemplateInterpreter(TemplateInterpreter.Arguments args) {
+        return new TemplateInterpreter(args);
+    }
 
     @Override
     public Script createExpression(final JexlInfo info, final String expression) {
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 9ff211a..226040e 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java
@@ -338,9 +338,6 @@ public class Interpreter extends InterpreterBase {
     protected Object visit(final ASTBitwiseOrNode node, final Object data) {
         final Object left = node.jjtGetChild(0).jjtAccept(this, data);
         final Object right = node.jjtGetChild(1).jjtAccept(this, data);
-        if (arithmetic.isStrict(JexlOperator.OR) && left == null || right == null) {
-            // boum
-        }
         try {
             final Object result = operators.tryOverload(node, JexlOperator.OR, left, right);
             return result != JexlEngine.TRY_FAILED ? result : arithmetic.or(left, right);
@@ -1091,7 +1088,7 @@ public class Interpreter extends InterpreterBase {
                 accessJxlt.setExpression(expr);
             }
             if (expr != null) {
-                final Object name = expr.evaluate(frame, context);
+                final Object name = expr.evaluate(context, frame, options);
                 if (name != null) {
                     final Integer id = ASTIdentifierAccess.parseIdentifier(name.toString());
                     return id != null ? id : name;
@@ -1178,12 +1175,9 @@ public class Interpreter extends InterpreterBase {
                     // *... and continue
                     if (!options.isAntish()) {
                         antish = false;
-                        continue;
                     }
+                    continue;
                     // skip the first node case since it was trialed in jjtAccept above and returned null
-                    if (c == 0) {
-                        continue;
-                    }
                 }
                 // catch up to current node
                 for (; v <= c; ++v) {
@@ -1790,7 +1784,7 @@ public class Interpreter extends InterpreterBase {
             node.jjtSetValue(tp);
         }
         if (tp != null) {
-            return tp.evaluate(frame, context);
+            return tp.evaluate(context, frame, options);
         }
         return null;
     }
diff --git a/src/main/java/org/apache/commons/jexl3/internal/Script.java b/src/main/java/org/apache/commons/jexl3/internal/Script.java
index 8e0d1c6..f121858 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Script.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Script.java
@@ -107,8 +107,18 @@ public class Script implements JexlScript, JexlExpression {
      * @return  the interpreter
      */
     protected Interpreter createInterpreter(final JexlContext context, final Frame frame) {
-        final JexlOptions opts = jexl.evalOptions(script, context);
-        return jexl.createInterpreter(context, frame, opts);
+        return createInterpreter(context, frame, null);
+    }
+
+    /**
+     * Creates this script interpreter.
+     * @param context the context
+     * @param frame the calling frame
+     * @param options the interpreter options
+     * @return  the interpreter
+     */
+    protected Interpreter createInterpreter(final JexlContext context, final Frame frame, final JexlOptions options) {
+        return jexl.createInterpreter(context, frame, options != null? options : jexl.evalOptions(script, context));
     }
 
     /**
@@ -221,6 +231,14 @@ public class Script implements JexlScript, JexlExpression {
     }
 
     /**
+     * Gets this script captured variable, i.e. symbols captured from outer scopes.
+     * @return the captured variable names
+     */
+    public String[] getCapturedVariables() {
+        return script.getCapturedVariables();
+    }
+
+    /**
      * @return the info
      */
     public JexlInfo getInfo() {
diff --git a/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java b/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java
index 55c032e..3c9bf99 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java
@@ -296,7 +296,7 @@ public final class TemplateEngine extends JxltEngine {
 
         @Override
         public final TemplateExpression prepare(final JexlContext context) {
-                return prepare(null, context);
+                return prepare(context, null, null);
         }
 
         /**
@@ -306,9 +306,10 @@ public final class TemplateEngine extends JxltEngine {
          * @return the expression value
          * @throws JexlException
          */
-        protected final TemplateExpression prepare(final Frame frame, final JexlContext context) {
+        protected final TemplateExpression prepare(final JexlContext context, final Frame frame, final JexlOptions opts) {
             try {
-                final Interpreter interpreter = jexl.createInterpreter(context, frame, jexl.evalOptions(context));
+                final JexlOptions interOptions = opts != null? opts : jexl.evalOptions(context);
+                final Interpreter interpreter = jexl.createInterpreter(context, frame, interOptions);
                 return prepare(interpreter);
             } catch (final JexlException xjexl) {
                 final JexlException xuel = createException(xjexl.getInfo(), "prepare", this, xjexl);
@@ -334,7 +335,7 @@ public final class TemplateEngine extends JxltEngine {
 
         @Override
         public final Object evaluate(final JexlContext context) {
-            return evaluate(null, context);
+            return evaluate(context, null, null);
         }
 
         /**
@@ -353,15 +354,14 @@ public final class TemplateEngine extends JxltEngine {
          * @return the expression value
          * @throws JexlException
          */
-        protected final Object evaluate(final Frame frame, final JexlContext context) {
+        protected final Object evaluate( final JexlContext context, final Frame frame, final JexlOptions options) {
             try {
-                final JexlOptions options = options(context);
                 final TemplateInterpreter.Arguments args = new TemplateInterpreter
                         .Arguments(jexl)
                         .context(context)
-                        .options(options)
+                        .options(options != null? options : options(context))
                         .frame(frame);
-                final Interpreter interpreter = new TemplateInterpreter(args);
+                final Interpreter interpreter = jexl.createTemplateInterpreter(args);
                 return evaluate(interpreter);
             } catch (final JexlException xjexl) {
                 final JexlException xuel = createException(xjexl.getInfo(), "evaluate", this, xjexl);
diff --git a/src/main/java/org/apache/commons/jexl3/internal/TemplateInterpreter.java b/src/main/java/org/apache/commons/jexl3/internal/TemplateInterpreter.java
index f3dffd8..4c15a51 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/TemplateInterpreter.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateInterpreter.java
@@ -148,7 +148,7 @@ public class TemplateInterpreter extends Interpreter {
         }
         TemplateEngine.TemplateExpression expr = exprs[e];
         if (expr.isDeferred()) {
-            expr = expr.prepare(frame, context);
+            expr = expr.prepare(context, frame, options);
         }
         if (expr instanceof TemplateEngine.CompositeExpression) {
             printComposite((TemplateEngine.CompositeExpression) expr);
@@ -268,15 +268,14 @@ public class TemplateInterpreter extends Interpreter {
         if (script instanceof ASTJexlLambda && !((ASTJexlLambda) script).isTopLevel()) {
             return new Closure(this, (ASTJexlLambda) script) {
                 @Override
-                protected Interpreter createInterpreter(final JexlContext context, final Frame local) {
-                    final JexlOptions opts = jexl.evalOptions(script, context);
+                protected Interpreter createInterpreter(final JexlContext context, final Frame local, final JexlOptions options) {
                     final TemplateInterpreter.Arguments targs = new TemplateInterpreter.Arguments(jexl)
                             .context(context)
-                            .options(opts)
+                            .options(options)
                             .frame(local)
                             .expressions(exprs)
                             .writer(writer);
-                    return new TemplateInterpreter(targs);
+                    return jexl.createTemplateInterpreter(targs);
                 }
             };
         }
diff --git a/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java b/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java
index 08d5d43..f71ca51 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java
@@ -264,7 +264,7 @@ public final class TemplateScript implements JxltEngine.Template {
                 .context(context)
                 .options(options)
                 .frame(frame);
-        final Interpreter interpreter = new TemplateInterpreter(targs);
+        final Interpreter interpreter = jexl.createTemplateInterpreter(targs);
         final TemplateExpression[] immediates = new TemplateExpression[exprs.length];
         for (int e = 0; e < exprs.length; ++e) {
             try {
@@ -300,7 +300,7 @@ public final class TemplateScript implements JxltEngine.Template {
                 .frame(frame)
                 .expressions(exprs)
                 .writer(writer);
-        final Interpreter interpreter = new TemplateInterpreter(targs);
+        final Interpreter interpreter = jexl.createTemplateInterpreter(targs);
         interpreter.interpret(script);
     }
 
diff --git a/src/test/java/org/apache/commons/jexl3/Issues300Test.java b/src/test/java/org/apache/commons/jexl3/Issues300Test.java
index b682f37..b6ce0b8 100644
--- a/src/test/java/org/apache/commons/jexl3/Issues300Test.java
+++ b/src/test/java/org/apache/commons/jexl3/Issues300Test.java
@@ -20,10 +20,14 @@ import java.io.StringReader;
 import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+
+import org.apache.commons.jexl3.internal.Engine32;
+import org.apache.commons.jexl3.internal.OptionsContext;
 import org.junit.Assert;
 import static org.junit.Assert.assertEquals;
 import org.junit.Test;
@@ -601,4 +605,141 @@ public class Issues300Test {
         JexlExpression expr = jexl.createExpression(text);
         JexlScript script = jexl.createScript(text);
     }
+
+    static JexlContext pragmaticContext() {
+        final JexlOptions opts = new JexlOptions();
+        opts.setFlags( "-strict", "-cancellable", "-lexical", "-lexicalShade", "+safe", "+sharedInstance");
+        return new JexlTestCase.PragmaticContext(opts);
+    }
+
+    @Test public void testPropagateOptions() throws Exception {
+        final String src0 = "`${$options.strict?'+':'-'}strict"
+                + " ${$options.cancellable?'+':'-'}cancellable"
+                + " ${$options.lexical?'+':'-'}lexical"
+                + " ${$options.lexicalShade?'+':'-'}lexicalShade"
+                + " ${$options.sharedInstance?'+':'-'}sharedInstance"
+                + " ${$options.safe?'+':'-'}safe`";
+        String text = "#pragma script.mode pro50\n" +
+                "()->{ ()->{ "+src0+"; } }";
+        JexlEngine jexl = new JexlBuilder().safe(true).create();
+        JexlScript script = jexl.createScript(text);
+        JexlContext context = pragmaticContext();
+        JexlScript closure = (JexlScript) script.execute(context);
+        JexlContext opts = new OptionsContext();
+        Object result = closure.execute(opts);
+        Assert.assertEquals("+strict +cancellable +lexical +lexicalShade -sharedInstance -safe", result);
+
+        String text0 = "#pragma script.mode pro50\n" +
+                "()->{ "+src0+"; }";
+        JexlScript script0 = jexl.createScript(text0);
+        context = pragmaticContext();
+        Object result0 = script0.execute(context);
+        Assert.assertEquals("+strict +cancellable +lexical +lexicalShade -sharedInstance -safe", result0);
+
+        String text1 = "#pragma script.mode pro50\n"+src0;
+        JexlScript script1 = jexl.createScript(text1);
+        context = pragmaticContext();
+        Object result1 = script1.execute(context);
+        Assert.assertEquals("+strict +cancellable +lexical +lexicalShade -sharedInstance -safe", result1);
+
+        String text2 = src0;
+        JexlScript script2 = jexl.createScript(text2);
+        context = pragmaticContext();
+        Object result2 = script2.execute(context);
+        Assert.assertEquals("-strict -cancellable -lexical -lexicalShade +sharedInstance +safe", result2);
+    }
+
+    @Test
+    public void tes361a_32() throws Exception {
+        JexlEngine jexl = new Engine32(new JexlBuilder().safe(false));
+        Object result  = run361a(jexl);
+        Assert.assertNotNull(result);
+    }
+
+    @Test
+    public void test361a_33() throws Exception {
+        JexlEngine jexl = new JexlBuilder().safe(false).strict(true).create();
+        try {
+            Object result = run361a(jexl);
+            Assert.fail("null arg should fail");
+        } catch(JexlException xany) {
+            Assert.assertNotNull(xany);
+        }
+    }
+
+    private Object run361a(JexlEngine jexl) throws Exception {
+        String src = "()-> { ()-> { if (versionFile != null) { return 'foo'; } else { return 'bar'; }} }";
+        JexlScript script = jexl.createScript(src);
+        Object result = script.execute(null);
+        JexlScript rs = (JexlScript) result;
+        return rs.execute(null);
+    }
+
+    @Test
+    public void test361b_33() throws Exception {
+        JexlEngine jexl = new JexlBuilder().safe(false).strict(true).create();
+        try {
+            Object result = run361b(jexl);
+            Assert.fail("null arg should fail");
+        } catch(JexlException xany) {
+            Assert.assertNotNull(xany);
+        }
+    }
+
+    @Test
+    public void test361b_32() {
+        JexlEngine jexl = new Engine32(new JexlBuilder().safe(false));
+        Object result = run361b(jexl);
+        Assert.assertNotNull(result);
+    }
+
+    private Object run361b(JexlEngine jexl) {
+        String src = "()-> { ()-> {" +
+                "var voa = vaf.value;\n" +
+                "if (voa != NaN && voa <= 0)" +
+                "{ return 'foo'; } else { return 'bar'; }" +
+                "} }";
+        MapContext context = new MapContext();
+        Map<String,Object> vaf = Collections.singletonMap("value", null);
+        context.set("vaf", vaf);
+        JexlScript script = jexl.createScript(src);
+        Object result = script.execute(null);
+        JexlScript rs = (JexlScript) result;
+        return rs.execute(context);
+    }
+
+    @Test
+    public void test361_33() {
+        JexlEngine jexl = new JexlBuilder().safe(false).strict(true).create();
+        try {
+            run361c(jexl);
+            Assert.fail("null arg should fail");
+        } catch(JexlException xany) {
+            Assert.assertNotNull(xany);
+        }
+    }
+
+    @Test
+    public void test361c_32() {
+        JexlEngine jexl = new Engine32(new JexlBuilder().safe(false));
+        String result = run361c(jexl);
+        Assert.assertNotNull(result);
+    }
+
+    private String run361c(JexlEngine jexl) {
+        String src = "$$var t = null;\n" +
+                "$$if (t < 0) {\n" +
+                "'foo'\n" +
+                "$$} else {\n" +
+                "'bar'\n" +
+                "$$}";
+        JxltEngine jxlt = jexl.createJxltEngine();
+        MapContext context = new MapContext();
+        Map<String,Object> vaf = Collections.singletonMap("value", null);
+        context.set("vaf", vaf);
+        JxltEngine.Template template = jxlt.createTemplate(src);
+        StringWriter strw = new StringWriter();
+        template.evaluate(context, strw);
+        return strw.toString();
+    }
 }
diff --git a/src/test/java/org/apache/commons/jexl3/JXLTTest.java b/src/test/java/org/apache/commons/jexl3/JXLTTest.java
index 7ecca69..6a65ba6 100644
--- a/src/test/java/org/apache/commons/jexl3/JXLTTest.java
+++ b/src/test/java/org/apache/commons/jexl3/JXLTTest.java
@@ -69,11 +69,10 @@ public class JXLTTest extends JexlTestCase {
    public static List<JexlBuilder> engines() {
        final JexlFeatures f = new JexlFeatures();
        f.lexical(true).lexicalShade(true);
-      return Arrays.<JexlBuilder>asList(new JexlBuilder().silent(false)
-        .lexical(true).lexicalShade(true)
-        .cache(128).strict(true), new JexlBuilder().features(f).silent(false)
-        .cache(128).strict(true), new JexlBuilder().silent(false)
-        .cache(128).strict(true));
+      return Arrays.<JexlBuilder>asList(
+              new JexlBuilder().silent(false).lexical(true).lexicalShade(true).cache(128).strict(true),
+              new JexlBuilder().features(f).silent(false).cache(128).strict(true),
+              new JexlBuilder().silent(false).cache(128).strict(true));
    }
 
     @Before
@@ -1021,40 +1020,6 @@ public class JXLTTest extends JexlTestCase {
         Assert.assertEquals(s315, output);
     }
 
-        // define mode pro50
-    static final JexlOptions MODE_PRO50 = new JexlOptions();
-    static {
-        MODE_PRO50.setFlags( "+strict +cancellable +lexical +lexicalShade -safe".split(" "));
-    }
-
-    public static class PragmaticContext extends MapContext implements JexlContext.PragmaProcessor, JexlContext.OptionsHandle {
-        private final JexlOptions options;
-
-        public PragmaticContext(final JexlOptions o) {
-            this.options = o;
-        }
-
-        @Override
-        public void processPragma(final String key, final Object value) {
-            if ("script.mode".equals(key) && "pro50".equals(value)) {
-                options.set(MODE_PRO50);
-            }
-        }
-
-        @Override
-        public Object get(final String name) {
-            if ("$options".equals(name)) {
-                return options;
-            }
-            return super.get(name);
-        }
-
-        @Override
-        public JexlOptions getEngineOptions() {
-            return options;
-        }
-    }
-
     @Test
     public void testLexicalTemplate() throws Exception {
         final JexlOptions opts = new JexlOptions();
@@ -1074,7 +1039,12 @@ public class JXLTTest extends JexlTestCase {
         final Writer strw0 = new StringWriter();
         tmplt0.evaluate(ctxt, strw0);
         final String output0 = strw0.toString();
-        Assert.assertEquals( "-strict -cancellable -lexical -lexicalShade +safe", output0);
+        JexlFeatures features = BUILDER.features();
+        if (features != null && features.isLexical() && features.isLexicalShade()) {
+            Assert.assertEquals("-strict -cancellable +lexical +lexicalShade +safe", output0);
+        } else {
+            Assert.assertEquals("-strict -cancellable -lexical -lexicalShade +safe", output0);
+        }
 
         final String src = "$$ #pragma script.mode pro50\n" + src0;
 
diff --git a/src/test/java/org/apache/commons/jexl3/JexlTestCase.java b/src/test/java/org/apache/commons/jexl3/JexlTestCase.java
index 56d031c..8a04c97 100644
--- a/src/test/java/org/apache/commons/jexl3/JexlTestCase.java
+++ b/src/test/java/org/apache/commons/jexl3/JexlTestCase.java
@@ -21,6 +21,8 @@ import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
 
 
+import org.apache.commons.jexl3.internal.Interpreter;
+import org.apache.commons.jexl3.internal.OptionsContext;
 import org.apache.commons.jexl3.internal.Util;
 import org.apache.commons.jexl3.internal.introspection.Permissions;
 import org.apache.commons.jexl3.internal.introspection.Uberspect;
@@ -69,6 +71,43 @@ public class JexlTestCase {
         return new JexlBuilder().create();
     }
 
+    // define mode pro50
+    static final JexlOptions MODE_PRO50 = new JexlOptions();
+    static {
+        MODE_PRO50.setFlags( "+strict +cancellable +lexical +lexicalShade -safe".split(" "));
+    }
+
+    public static class PragmaticContext extends OptionsContext implements JexlContext.PragmaProcessor, JexlContext.OptionsHandle {
+        private final JexlOptions options;
+
+        public PragmaticContext() {
+            this(new JexlOptions());
+        }
+
+        public PragmaticContext(final JexlOptions o) {
+            super();
+            this.options = o;
+        }
+
+        @Override
+        public void processPragma(String key, Object value) {
+            processPragma(null, key, value);
+        }
+
+        @Override
+        public void processPragma(JexlOptions opts, final String key, final Object value) {
+            if ("script.mode".equals(key) && "pro50".equals(value)) {
+                opts.set(MODE_PRO50);
+            }
+        }
+
+        @Override
+        public JexlOptions getEngineOptions() {
+            return options;
+        }
+    }
+
+
     /**
      * A very secure singleton.
      */
diff --git a/src/test/java/org/apache/commons/jexl3/LambdaTest.java b/src/test/java/org/apache/commons/jexl3/LambdaTest.java
index 328e89b..34bed32 100644
--- a/src/test/java/org/apache/commons/jexl3/LambdaTest.java
+++ b/src/test/java/org/apache/commons/jexl3/LambdaTest.java
@@ -16,16 +16,19 @@
  */
 package org.apache.commons.jexl3;
 
+import org.apache.commons.jexl3.internal.Script;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Callable;
-import org.junit.Assert;
-import org.junit.Test;
 
 /**
  * Tests function/lambda/closure features.
  */
-@SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"})
+@SuppressWarnings({"AssertEqualsBetweenInconvertibleTypes"})
 public class LambdaTest extends JexlTestCase {
 
     public LambdaTest() {
@@ -33,7 +36,7 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void testScriptArguments() throws Exception {
+    public void testScriptArguments() {
         final JexlEngine jexl = createEngine();
         final JexlScript s = jexl.createScript(" x + x ", "x");
         final JexlScript s42 = jexl.createScript("s(21)", "s");
@@ -42,14 +45,14 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void testScriptContext() throws Exception {
+    public void testScriptContext() {
         final JexlEngine jexl = createEngine();
         final JexlScript s = jexl.createScript("function(x) { x + x }");
         final String fsstr = s.getParsedText(0);
         Assert.assertEquals("(x)->{ x + x; }", fsstr);
         Assert.assertEquals(42, s.execute(null, 21));
         JexlScript s42 = jexl.createScript("s(21)");
-        final JexlEvalContext ctxt = new JexlEvalContext();
+        final JexlContext ctxt = new JexlEvalContext();
         ctxt.set("s", s);
         Object result = s42.execute(ctxt);
         Assert.assertEquals(42, result);
@@ -61,7 +64,7 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void testLambda() throws Exception {
+    public void testLambda() {
         final JexlEngine jexl = createEngine();
         String strs = "var s = function(x) { x + x }; s(21)";
         JexlScript s42 = jexl.createScript(strs);
@@ -74,7 +77,7 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void testLambdaClosure() throws Exception {
+    public void testLambdaClosure()  {
         final JexlEngine jexl = createEngine();
         String strs = "var t = 20; var s = function(x, y) { x + y + t}; s(15, 7)";
         JexlScript s42 = jexl.createScript(strs);
@@ -95,7 +98,7 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void testLambdaLambda() throws Exception {
+    public void testLambdaLambda() {
         final JexlEngine jexl = createEngine();
         String strs = "var t = 19; ( (x, y)->{ var t = 20; x + y + t} )(15, 7);";
         JexlScript s42 = jexl.createScript(strs);
@@ -114,7 +117,7 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void testNestLambda() throws Exception {
+    public void testNestLambda() {
         final JexlEngine jexl = createEngine();
         final String strs = "( (x)->{ (y)->{ x + y } })(15)(27)";
         final JexlScript s42 = jexl.createScript(strs);
@@ -125,28 +128,27 @@ public class LambdaTest extends JexlTestCase {
     @Test
     public void testNestLambada() throws Exception {
         final JexlEngine jexl = createEngine();
-        final JexlContext ctx = null;
         final String strs = "(x)->{ (y)->{ x + y } }";
         final JexlScript s42 = jexl.createScript(strs);
         final JexlScript s42b = jexl.createScript(s42.toString());
         Assert.assertEquals(s42.hashCode(), s42b.hashCode());
         Assert.assertEquals(s42, s42b);
-        Object result = s42.execute(ctx, 15);
+        Object result = s42.execute(null, 15);
         Assert.assertTrue(result instanceof JexlScript);
-        final Object resultb = s42.execute(ctx, 15);
+        final Object resultb = s42.execute(null, 15);
         Assert.assertEquals(result.hashCode(), resultb.hashCode());
         Assert.assertEquals(result, resultb);
-        Assert.assertEquals(result, jexl.createScript(resultb.toString(), "x").execute(ctx, 15));
+        Assert.assertEquals(result, jexl.createScript(resultb.toString(), "x").execute(null, 15));
         final JexlScript s15 = (JexlScript) result;
-        final Callable<Object> s15b = s15.callable(ctx, 27);
-        result = s15.execute(ctx, 27);
+        final Callable<Object> s15b = s15.callable(null, 27);
+        result = s15.execute(null, 27);
         Assert.assertEquals(42, result);
         result = s15b.call();
         Assert.assertEquals(42, result);
     }
 
     @Test
-    public void testHoistLambda() throws Exception {
+    public void testHoistLambda() {
         final JexlEngine jexl = createEngine();
         final JexlEvalContext ctx = new JexlEvalContext();
         ctx.getEngineOptions().setLexical(false);
@@ -187,55 +189,57 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void testRecurse() throws Exception {
+    public void testRecurse() {
+        final JexlEngine jexl = createEngine();
+        final JexlContext jc = new MapContext();
+        final JexlScript script = jexl.createScript("var fact = (x)->{ if (x <= 1) 1; else x * fact(x - 1) }; fact(5)");
+        final int result = (Integer) script.execute(jc);
+        Assert.assertEquals(120, result);
+    }
+
+    @Test
+    public void testRecurse2() {
         final JexlEngine jexl = createEngine();
         final JexlContext jc = new MapContext();
-        try {
-            final JexlScript script = jexl.createScript("var fact = (x)->{ if (x <= 1) 1; else x * fact(x - 1) }; fact(5)");
-            final int result = (Integer) script.execute(jc);
-            Assert.assertEquals(120, result);
-        } catch (final JexlException xany) {
-            final String msg = xany.toString();
-            throw xany;
-        }
+        // adding some captured vars to get it confused
+        final JexlScript script = jexl.createScript(
+                "var y = 1; var z = 1; "
+                +"var fact = (x)->{ if (x <= y) z; else x * fact(x - 1) }; fact(6)");
+        final int result = (Integer) script.execute(jc);
+        Assert.assertEquals(720, result);
     }
 
     @Test
-    public void testRecurse2() throws Exception {
+    public void testRecurse2b() {
         final JexlEngine jexl = createEngine();
         final JexlContext jc = new MapContext();
         // adding some captured vars to get it confused
-        try {
-            final JexlScript script = jexl.createScript(
-                    "var y = 1; var z = 1; "
-                    +"var fact = (x)->{ if (x <= y) z; else x * fact(x - 1) }; fact(6)");
-            final int result = (Integer) script.execute(jc);
-            Assert.assertEquals(720, result);
-        } catch (final JexlException xany) {
-            final String msg = xany.toString();
-            throw xany;
-        }
+        final JexlScript fact = jexl.createScript(
+                "var y = 1; var z = 1; "
+                        +"var fact = (x)->{ if (x <= y) z; else x * fact(x - 1) };" +
+                        "fact");
+        Script func = (Script) fact.execute(jc);
+        String[] captured = func.getCapturedVariables();
+        Assert.assertEquals(3, captured.length);
+        Assert.assertTrue(Arrays.asList(captured).containsAll(Arrays.asList("z", "y", "fact")));
+        final int result = (Integer) func.execute(jc, 6);
+        Assert.assertEquals(720, result);
     }
 
     @Test
-    public void testRecurse3() throws Exception {
+    public void testRecurse3() {
         final JexlEngine jexl = createEngine();
         final JexlContext jc = new MapContext();
         // adding some captured vars to get it confused
-        try {
-            final JexlScript script = jexl.createScript(
-                    "var y = 1; var z = 1;var foo = (x)->{y + z}; "
-                    +"var fact = (x)->{ if (x <= y) z; else x * fact(x - 1) }; fact(6)");
-            final int result = (Integer) script.execute(jc);
-            Assert.assertEquals(720, result);
-        } catch (final JexlException xany) {
-            final String msg = xany.toString();
-            throw xany;
-        }
+        final JexlScript script = jexl.createScript(
+                "var y = 1; var z = 1;var foo = (x)->{y + z}; "
+                +"var fact = (x)->{ if (x <= y) z; else x * fact(x - 1) }; fact(6)");
+        final int result = (Integer) script.execute(jc);
+        Assert.assertEquals(720, result);
     }
 
     @Test
-    public void testIdentity() throws Exception {
+    public void testIdentity() {
         final JexlEngine jexl = createEngine();
         JexlScript script;
         Object result;
@@ -247,7 +251,7 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void testCurry1() throws Exception {
+    public void testCurry1() {
         final JexlEngine jexl = createEngine();
         JexlScript script;
         Object result;
@@ -270,7 +274,7 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void testCurry2() throws Exception {
+    public void testCurry2() {
         final JexlEngine jexl = createEngine();
         JexlScript script;
         Object result;
@@ -286,7 +290,7 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void testCurry3() throws Exception {
+    public void testCurry3() {
         final JexlEngine jexl = createEngine();
         JexlScript script;
         Object result;
@@ -298,7 +302,7 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void testCurry4() throws Exception {
+    public void testCurry4() {
         final JexlEngine jexl = createEngine();
         JexlScript script;
         Object result;
@@ -310,7 +314,7 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void testCurry5() throws Exception {
+    public void testCurry5() {
         final JexlEngine jexl = createEngine();
         JexlScript script;
         Object result;
@@ -322,14 +326,14 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void test270() throws Exception {
+    public void test270() {
         final JexlEngine jexl = createEngine();
         final JexlScript base = jexl.createScript("(x, y, z)->{ x + y + z }");
         final String text = base.toString();
         JexlScript script = base.curry(5, 15);
         Assert.assertEquals(text, script.toString());
 
-        final JexlEvalContext ctxt = new JexlEvalContext();
+        final JexlContext ctxt = new JexlEvalContext();
         ctxt.set("s", base);
         script = jexl.createScript("return s");
         Object result = script.execute(ctxt);
@@ -341,7 +345,7 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void test271a() throws Exception {
+    public void test271a() {
         final JexlEngine jexl = createEngine();
         final JexlScript base = jexl.createScript("var base = 1; var x = (a)->{ var y = (b) -> {base + b}; return base + y(a)}; x(40)");
         final Object result = base.execute(null);
@@ -349,7 +353,7 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void test271b() throws Exception {
+    public void test271b() {
         final JexlEngine jexl = createEngine();
         final JexlScript base = jexl.createScript("var base = 2; var sum = (x, y, z)->{ base + x + y + z }; var y = sum.curry(1); y(2,3)");
         final Object result = base.execute(null);
@@ -357,7 +361,7 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void test271c() throws Exception {
+    public void test271c() {
         final JexlEngine jexl = createEngine();
         final JexlScript base = jexl.createScript("(x, y, z)->{ 2 + x + y + z };");
         final JexlScript y = base.curry(1);
@@ -366,7 +370,7 @@ public class LambdaTest extends JexlTestCase {
     }
 
     @Test
-    public void test271d() throws Exception {
+    public void test271d() {
         final JexlEngine jexl = createEngine();
         final JexlScript base = jexl.createScript("var base = 2; return (x, y, z)->{ base + x + y + z };");
         final JexlScript y = ((JexlScript) base.execute(null)).curry(1);
@@ -376,11 +380,11 @@ public class LambdaTest extends JexlTestCase {
 
     // redefining an captured var is not resolved correctly in left hand side;
     // declare the var in local frame, resolved in local frame instead of parent
-//    @Test
-//    public void test271e() throws Exception {
-//        JexlEngine jexl = createEngine();
-//        JexlScript base = jexl.createScript("var base = 1000; var f = (x, y)->{ var base = x + y + (base?:-1000); base; }; f(100, 20)");
-//        Object result = base.execute(null);
-//        Assert.assertEquals(-880, result);
-//    }
+    @Test
+    public void test271e() {
+        JexlEngine jexl = createEngine();
+        JexlScript base = jexl.createScript("var base = 1000; var f = (x, y)->{ var base = x + y + (base?:-1000); base; }; f(100, 20)");
+        Object result = base.execute(null);
+        Assert.assertEquals(1120, result);
+    }
 }
diff --git a/src/test/java/org/apache/commons/jexl3/internal/OptionsContext.java b/src/test/java/org/apache/commons/jexl3/internal/OptionsContext.java
new file mode 100644
index 0000000..ee24979
--- /dev/null
+++ b/src/test/java/org/apache/commons/jexl3/internal/OptionsContext.java
@@ -0,0 +1,38 @@
+/*
+ * 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.MapContext;
+
+/**
+ * A map context that has access to the interpreter evaluation options.
+ */
+public class OptionsContext extends MapContext {
+    @Override
+    public Object get(final String name) {
+        if ("$options".equals(name)) {
+            Interpreter pinter = Interpreter.INTER.get();
+            return pinter.options;
+        }
+        return super.get(name);
+    }
+
+    @Override
+    public boolean has(final String name) {
+        return "$options".equals(name) || super.has(name);
+    }
+}