You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2019/05/25 23:02:14 UTC

[freemarker] branch 2.3-gae updated (1e16a4f -> a2ed41d)

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

ddekany pushed a change to branch 2.3-gae
in repository https://gitbox.apache.org/repos/asf/freemarker.git.


    from 1e16a4f  Adjustments to recently merged BeansWrapper.clearClassIntrospecitonCache -> clearClassIntrospectionCache typo fix
     new 33d6e9a  exp[rangeExp] operator now supports lazily generated sequences as input. For now it always gives lazily generated output in that case, but that's incorrect and will change.
     new 1f25c8b  Generalized/simplified the way it's decided if an expression can produce or consume lazily generated values.
     new a2ed41d  exp[rangeExp] now returns sequence or lazily generated sequence depending on what the consumer supports. Also added size bypassing through exp[rangeExp] (like in seq?map(f)[10..]?size no seq element will be consumed, as the size can be calculated without that).

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../java/freemarker/core/BoundedRangeModel.java    |   2 +-
 src/main/java/freemarker/core/BuiltIn.java         |  13 +-
 .../freemarker/core/BuiltInsForMultipleTypes.java  |   2 +-
 .../java/freemarker/core/BuiltInsForSequences.java |  46 ++--
 src/main/java/freemarker/core/DynamicKeyName.java  | 244 ++++++++++++++++++---
 src/main/java/freemarker/core/Expression.java      |  18 ++
 src/main/java/freemarker/core/IteratorBlock.java   |  10 +-
 .../core/LazilyGeneratedSequenceModel.java         |   6 +-
 ...a => LazilyGeneratedSequenceModelWithSize.java} |  20 +-
 .../freemarker/core/ParentheticalExpression.java   |   7 +-
 src/main/java/freemarker/core/RangeModel.java      |   2 +-
 .../freemarker/core/RightUnboundedRangeModel.java  |   2 +-
 .../core/SingleIterationCollectionModel.java       |   2 +-
 src/main/java/freemarker/core/_CoreAPI.java        |   5 +-
 .../freemarker/template/utility/ClassUtil.java     |   3 +-
 src/main/resources/freemarker/included.html        |   9 +
 ...iltinsTest.java => LazilyGeneratedSeqTest.java} |  73 +++++-
 .../test/templatesuite/templates/range-lazy.ftl    |  58 +++++
 .../freemarker/test/templatesuite/testcases.xml    |   5 +-
 19 files changed, 427 insertions(+), 100 deletions(-)
 copy src/main/java/freemarker/core/{NonListableRightUnboundedRangeModel.java => LazilyGeneratedSequenceModelWithSize.java} (67%)
 create mode 100644 src/main/resources/freemarker/included.html
 rename src/test/java/freemarker/core/{LazilyGeneratedSeqTargetSupportInBuiltinsTest.java => LazilyGeneratedSeqTest.java} (67%)
 create mode 100644 src/test/resources/freemarker/test/templatesuite/templates/range-lazy.ftl


[freemarker] 01/03: exp[rangeExp] operator now supports lazily generated sequences as input. For now it always gives lazily generated output in that case, but that's incorrect and will change.

Posted by dd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch 2.3-gae
in repository https://gitbox.apache.org/repos/asf/freemarker.git

commit 33d6e9a7feaa615f1cb7f19365c0c715e77cdaa3
Author: ddekany <dd...@apache.org>
AuthorDate: Sat Apr 6 12:37:20 2019 +0200

    exp[rangeExp] operator now supports lazily generated sequences as input. For now it always gives lazily generated output in that case, but that's incorrect and will change.
---
 .../java/freemarker/core/BoundedRangeModel.java    |   2 +-
 src/main/java/freemarker/core/DynamicKeyName.java  | 213 +++++++++++++++++----
 src/main/java/freemarker/core/RangeModel.java      |   2 +-
 .../freemarker/core/RightUnboundedRangeModel.java  |   2 +-
 src/main/java/freemarker/core/_CoreAPI.java        |   5 +-
 .../freemarker/template/utility/ClassUtil.java     |   3 +-
 src/main/resources/freemarker/included.html        |   9 +
 ...iltinsTest.java => LazilyGeneratedSeqTest.java} |  26 ++-
 .../test/templatesuite/templates/range-lazy.ftl    |  58 ++++++
 .../freemarker/test/templatesuite/testcases.xml    |   5 +-
 10 files changed, 285 insertions(+), 40 deletions(-)

diff --git a/src/main/java/freemarker/core/BoundedRangeModel.java b/src/main/java/freemarker/core/BoundedRangeModel.java
index 4240e89..84e2f28 100644
--- a/src/main/java/freemarker/core/BoundedRangeModel.java
+++ b/src/main/java/freemarker/core/BoundedRangeModel.java
@@ -62,7 +62,7 @@ final class BoundedRangeModel extends RangeModel {
     }
 
     @Override
-    boolean isAffactedByStringSlicingBug() {
+    boolean isAffectedByStringSlicingBug() {
         return affectedByStringSlicingBug;
     }
     
diff --git a/src/main/java/freemarker/core/DynamicKeyName.java b/src/main/java/freemarker/core/DynamicKeyName.java
index d369e8e..c01d0ef 100644
--- a/src/main/java/freemarker/core/DynamicKeyName.java
+++ b/src/main/java/freemarker/core/DynamicKeyName.java
@@ -24,9 +24,12 @@ import java.util.Collections;
 
 import freemarker.template.SimpleScalar;
 import freemarker.template.SimpleSequence;
+import freemarker.template.TemplateCollectionModelEx;
 import freemarker.template.TemplateException;
 import freemarker.template.TemplateHashModel;
 import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateModelIterator;
 import freemarker.template.TemplateNumberModel;
 import freemarker.template.TemplateScalarModel;
 import freemarker.template.TemplateSequenceModel;
@@ -39,12 +42,20 @@ import freemarker.template.utility.Constants;
  */
 final class DynamicKeyName extends Expression {
 
+    private static final int UNKNOWN_RESULT_SIZE = -1;
+
     private final Expression keyExpression;
     private final Expression target;
 
     DynamicKeyName(Expression target, Expression keyExpression) {
         this.target = target; 
         this.keyExpression = keyExpression;
+
+        Expression cleanedTarget = MiscUtil.peelParentheses(target);
+        if (cleanedTarget instanceof BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) {
+            ((BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) cleanedTarget)
+                    .setLazyResultGenerationAllowed(true);
+        }
     }
 
     @Override
@@ -103,8 +114,19 @@ final class DynamicKeyName extends Expression {
                 size = Integer.MAX_VALUE;
             }
             return index < size ? tsm.get(index) : null;
-        } 
-        
+        }
+        if (targetModel instanceof LazilyGeneratedSequenceModel) {
+            TemplateModelIterator iter = ((LazilyGeneratedSequenceModel) targetModel).iterator();
+            for (int curIndex = 0; iter.hasNext(); curIndex++) {
+                TemplateModel next = iter.next();
+                if (index == curIndex) {
+                    return next;
+                }
+            }
+            return null;
+        }
+
+        // Fall back to get a character from a string
         try {
             String s = target.evalAndCoerceToPlainText(env);
             try {
@@ -143,14 +165,22 @@ final class DynamicKeyName extends Expression {
     }
 
     private TemplateModel dealWithRangeKey(TemplateModel targetModel, RangeModel range, Environment env)
-    throws UnexpectedTypeException, InvalidReferenceException, TemplateException {
+    throws TemplateException {
+        // We can have 3 kind of left hand operands ("targets"): sequence, lazyily generated sequence, string
         final TemplateSequenceModel targetSeq;
+        final LazilyGeneratedSequenceModel targetLazySeq;
         final String targetStr;
         if (targetModel instanceof TemplateSequenceModel) {
             targetSeq = (TemplateSequenceModel) targetModel;
+            targetLazySeq = null;
+            targetStr = null;
+        } else if (targetModel instanceof LazilyGeneratedSequenceModel) {
+            targetSeq = null;
+            targetLazySeq = (LazilyGeneratedSequenceModel) targetModel;
             targetStr = null;
         } else {
             targetSeq = null;
+            targetLazySeq = null;
             try {
                 targetStr = target.evalAndCoerceToPlainText(env);
             } catch (NonStringException e) {
@@ -161,13 +191,13 @@ final class DynamicKeyName extends Expression {
             }
         }
         
-        final int size = range.size();
+        final int rangeSize = range.size(); // Warning: Value is meaningless for right unbounded sequences
         final boolean rightUnbounded = range.isRightUnbounded();
-        final boolean rightAdaptive = range.isRightAdaptive();
+        final boolean rightAdaptive = range.isRightAdaptive(); // Always true if rightUnbounded
         
-        // Right bounded empty ranges are accepted even if the begin index is out of bounds. That's because a such range
+        // Empty ranges are accepted even if the begin index is out of bounds. That's because a such range
         // produces an empty sequence, which thus doesn't contain any illegal indexes.
-        if (!rightUnbounded && size == 0) {
+        if (!rightUnbounded && rangeSize == 0) {
             return emptyResult(targetSeq != null);
         }
 
@@ -177,26 +207,45 @@ final class DynamicKeyName extends Expression {
                     "Negative range start index (", Integer.valueOf(firstIdx),
                     ") isn't allowed for a range used for slicing.");
         }
-        
-        final int targetSize = targetStr != null ? targetStr.length() : targetSeq.size();
-        final int step = range.getStep();
-        
-        // Right-adaptive increasing ranges can start 1 after the last element of the target, because they are like
-        // ranges with exclusive end index of at most targetSize. Thence a such range is just an empty list of indexes,
-        // and thus it isn't out-of-bounds.
-        // Right-adaptive decreasing ranges has exclusive end -1, so it can't help on a  to high firstIndex. 
-        // Right-bounded ranges at this point aren't empty, so the right index surely can't reach targetSize. 
-        if (rightAdaptive && step == 1 ? firstIdx > targetSize : firstIdx >= targetSize) {
-            throw new _MiscTemplateException(keyExpression,
-                    "Range start index ", Integer.valueOf(firstIdx), " is out of bounds, because the sliced ",
-                    (targetStr != null ? "string" : "sequence"),
-                    " has only ", Integer.valueOf(targetSize), " ", (targetStr != null ? "character(s)" : "element(s)"),
-                    ". ", "(Note that indices are 0-based).");
+
+        final int step = range.getStep(); // Currently always 1 or -1
+
+        final int targetSize;
+        final boolean targetSizeKnown; // Didn't want to use targetSize = -1, as we don't control the seq.size() impl.
+        if (targetStr != null) {
+            targetSize = targetStr.length();
+            targetSizeKnown = true;
+        } else if (targetSeq != null) {
+            targetSize = targetSeq.size();
+            targetSizeKnown = true;
+        } else if (targetLazySeq instanceof TemplateCollectionModelEx) {
+            // E.g. the size of seq?map(f) is known, despite that the elements are lazily calculated.
+            targetSize = ((TemplateCollectionModelEx) targetLazySeq).size();
+            targetSizeKnown = true;
+        } else {
+            targetSize = Integer.MAX_VALUE;
+            targetSizeKnown = false;
+        }
+
+        if (targetSizeKnown) {
+            // Right-adaptive increasing ranges can start 1 after the last element of the target, because they are like
+            // ranges with exclusive end index of at most targetSize. Hence a such range is just an empty list of
+            // indexes, and thus it isn't out-of-bounds.
+            // Right-adaptive decreasing ranges has exclusive end -1, so it can't help on a too high firstIndex.
+            // Right-bounded ranges at this point aren't empty, so firstIndex == targetSize can't be allowed.
+            if (rightAdaptive && step == 1 ? firstIdx > targetSize : firstIdx >= targetSize) {
+                throw new _MiscTemplateException(keyExpression,
+                        "Range start index ", Integer.valueOf(firstIdx), " is out of bounds, because the sliced ",
+                        (targetStr != null ? "string" : "sequence"),
+                        " has only ", Integer.valueOf(targetSize), " ",
+                        (targetStr != null ? "character(s)" : "element(s)"),
+                        ". ", "(Note that indices are 0-based).");
+            }
         }
         
-        final int resultSize;
+        final int resultSize; // Might will be UNKNOWN_RESULT_SIZE, when targetLazySeq != null
         if (!rightUnbounded) {
-            final int lastIdx = firstIdx + (size - 1) * step;
+            final int lastIdx = firstIdx + (rangeSize - 1) * step; // Note: lastIdx is inclusive
             if (lastIdx < 0) {
                 if (!rightAdaptive) {
                     throw new _MiscTemplateException(keyExpression,
@@ -205,7 +254,7 @@ final class DynamicKeyName extends Expression {
                 } else {
                     resultSize = firstIdx + 1;
                 }
-            } else if (lastIdx >= targetSize) {
+            } else if (targetSizeKnown && lastIdx >= targetSize) {
                 if (!rightAdaptive) {
                     throw new _MiscTemplateException(keyExpression,
                             "Range end index ", Integer.valueOf(lastIdx), " is out of bounds, because the sliced ",
@@ -216,28 +265,36 @@ final class DynamicKeyName extends Expression {
                     resultSize = Math.abs(targetSize - firstIdx);
                 }
             } else {
-                resultSize = size;
+                resultSize = rangeSize;
             }
-        } else {
-            resultSize = targetSize - firstIdx;
+        } else { // rightUnbounded
+            resultSize = targetSizeKnown ? targetSize - firstIdx : UNKNOWN_RESULT_SIZE;
         }
         
         if (resultSize == 0) {
             return emptyResult(targetSeq != null);
         }
         if (targetSeq != null) {
-            ArrayList/*<TemplateModel>*/ list = new ArrayList(resultSize);
+            ArrayList<TemplateModel> resultList = new ArrayList<TemplateModel>(resultSize);
             int srcIdx = firstIdx;
             for (int i = 0; i < resultSize; i++) {
-                list.add(targetSeq.get(srcIdx));
+                resultList.add(targetSeq.get(srcIdx));
                 srcIdx += step;
             }
             // List items are already wrapped, so the wrapper will be null:
-            return new SimpleSequence(list, null);
+            return new SimpleSequence(resultList, null);
+        } else if (targetLazySeq != null) {
+            if (step == 1) {
+                return getStep1RangeFromIterator(targetLazySeq.iterator(), range, resultSize);
+            } else if (step == -1) {
+                return getStepMinus1RangeFromIterator(targetLazySeq.iterator(), range, resultSize);
+            } else {
+                throw new AssertionError();
+            }
         } else {
             final int exclEndIdx;
             if (step < 0 && resultSize > 1) {
-                if (!(range.isAffactedByStringSlicingBug() && resultSize == 2)) {
+                if (!(range.isAffectedByStringSlicingBug() && resultSize == 2)) {
                     throw new _MiscTemplateException(keyExpression,
                             "Decreasing ranges aren't allowed for slicing strings (as it would give reversed text). "
                             + "The index range was: first = ", Integer.valueOf(firstIdx),
@@ -255,6 +312,98 @@ final class DynamicKeyName extends Expression {
         }
     }
 
+    private TemplateModel getStep1RangeFromIterator(final TemplateModelIterator targetIter, final RangeModel range, int resultSize)
+            throws TemplateModelException {
+        final int firstIdx = range.getBegining();
+        final int lastIdx = firstIdx + (range.size() - 1); // Note: meaningless if the range is right unbounded
+        final boolean rightAdaptive = range.isRightAdaptive();
+        final boolean rightUnbounded = range.isRightUnbounded();
+        return new LazilyGeneratedSequenceModel(new TemplateModelIterator() {
+            private boolean elementsBeforeFirsIndexWereSkipped;
+            private int nextIdx;
+
+            public TemplateModel next() throws TemplateModelException {
+                ensureElementsBeforeFirstIndexWereSkipped();
+                if (!rightUnbounded && nextIdx > lastIdx) {
+                    throw new _TemplateModelException(
+                            "Iterator has no more elements (at index ", Integer.valueOf(nextIdx), ")");
+                }
+                if (!rightAdaptive && !targetIter.hasNext()) {
+                    // We fail because the range was wrong, not because this iterator was over-consumed.
+                    throw new _TemplateModelException(keyExpression,
+                            "Range end index ", Integer.valueOf(lastIdx), " is out of bounds, as sliced sequence " +
+                            "only has ", nextIdx, " elements.");
+                }
+                TemplateModel result = targetIter.next();
+                nextIdx++;
+                return result;
+            }
+
+            public boolean hasNext() throws TemplateModelException {
+                ensureElementsBeforeFirstIndexWereSkipped();
+                return (rightUnbounded || nextIdx <= lastIdx) && (!rightAdaptive || targetIter.hasNext());
+            }
+
+            public void ensureElementsBeforeFirstIndexWereSkipped() throws TemplateModelException {
+                if (elementsBeforeFirsIndexWereSkipped) {
+                    return;
+                }
+
+                while (nextIdx < firstIdx) {
+                    if (!targetIter.hasNext()) {
+                        throw new _TemplateModelException(keyExpression,
+                                "Range start index ", Integer.valueOf(firstIdx), " is out of bounds, as the sliced " +
+                                "sequence only has ", nextIdx, " elements.");
+                    }
+                    targetIter.next();
+                    nextIdx++;
+                }
+                elementsBeforeFirsIndexWereSkipped = true;
+            }
+        });
+    }
+
+    // Because the order has to be reversed, we have to "buffer" the stream in this case.
+    private TemplateModel getStepMinus1RangeFromIterator(TemplateModelIterator targetIter, RangeModel range,
+            int resultSize)
+            throws TemplateException {
+        int highIndex = range.getBegining();
+        // Low index was found to be valid earlier. So now something like [2..*-9] becomes to [2..0] in effect.
+        int lowIndex = Math.max(highIndex - (range.size() - 1), 0);
+
+        final TemplateModel[] resultElements = new TemplateModel[highIndex - lowIndex + 1];
+
+        int srcIdx = 0; // With an Iterator we can only start from index 0
+        int dstIdx = resultElements.length - 1; // Write into the result array backwards
+        while (srcIdx <= highIndex && targetIter.hasNext()) {
+            TemplateModel element = targetIter.next();
+            if (srcIdx >= lowIndex) {
+                resultElements[dstIdx--] = element;
+            }
+            srcIdx++;
+        }
+        if (dstIdx != -1) {
+            throw new _MiscTemplateException(DynamicKeyName.this,
+                    "Range top index " + highIndex + " (0-based) is outside the sliced sequence of length " +
+                    srcIdx + ".");
+        }
+        return new LazilyGeneratedSequenceModel(new TemplateModelIterator() {
+            private int nextIndex;
+
+            public TemplateModel next() throws TemplateModelException {
+                try {
+                    return resultElements[nextIndex++];
+                } catch (IndexOutOfBoundsException e) {
+                    throw new TemplateModelException("There are no more elements in the iterator");
+                }
+            }
+
+            public boolean hasNext() throws TemplateModelException {
+                return nextIndex < resultElements.length;
+            }
+        });
+    }
+
     private TemplateModel emptyResult(boolean seq) {
         return seq
                 ? (_TemplateAPI.getTemplateLanguageVersionAsInt(this) < _TemplateAPI.VERSION_INT_2_3_21
diff --git a/src/main/java/freemarker/core/RangeModel.java b/src/main/java/freemarker/core/RangeModel.java
index 7e526e0..e4f55a2 100644
--- a/src/main/java/freemarker/core/RangeModel.java
+++ b/src/main/java/freemarker/core/RangeModel.java
@@ -53,6 +53,6 @@ abstract class RangeModel implements TemplateSequenceModel, java.io.Serializable
     
     abstract boolean isRightAdaptive();
     
-    abstract boolean isAffactedByStringSlicingBug();
+    abstract boolean isAffectedByStringSlicingBug();
 
 }
diff --git a/src/main/java/freemarker/core/RightUnboundedRangeModel.java b/src/main/java/freemarker/core/RightUnboundedRangeModel.java
index fb1559d..2349d53 100644
--- a/src/main/java/freemarker/core/RightUnboundedRangeModel.java
+++ b/src/main/java/freemarker/core/RightUnboundedRangeModel.java
@@ -41,7 +41,7 @@ abstract class RightUnboundedRangeModel extends RangeModel {
     }
 
     @Override
-    final boolean isAffactedByStringSlicingBug() {
+    final boolean isAffectedByStringSlicingBug() {
         return false;
     }
     
diff --git a/src/main/java/freemarker/core/_CoreAPI.java b/src/main/java/freemarker/core/_CoreAPI.java
index 3491568..7b0596b 100644
--- a/src/main/java/freemarker/core/_CoreAPI.java
+++ b/src/main/java/freemarker/core/_CoreAPI.java
@@ -218,5 +218,8 @@ public class _CoreAPI {
     public static void setPreventStrippings(FMParser parser, boolean preventStrippings) {
         parser.setPreventStrippings(preventStrippings);
     }
-    
+
+    public static boolean isLazilyGeneratedSequenceModel(Class cl) {
+        return LazilyGeneratedSequenceModel.class.isAssignableFrom(cl);
+    }
 }
diff --git a/src/main/java/freemarker/template/utility/ClassUtil.java b/src/main/java/freemarker/template/utility/ClassUtil.java
index 66944fe..45508df 100644
--- a/src/main/java/freemarker/template/utility/ClassUtil.java
+++ b/src/main/java/freemarker/template/utility/ClassUtil.java
@@ -29,6 +29,7 @@ import java.util.Set;
 import freemarker.core.Environment;
 import freemarker.core.Macro;
 import freemarker.core.TemplateMarkupOutputModel;
+import freemarker.core._CoreAPI;
 import freemarker.ext.beans.BeanModel;
 import freemarker.ext.beans.BooleanModel;
 import freemarker.ext.beans.CollectionModel;
@@ -207,7 +208,7 @@ public class ClassUtil {
             appendTypeName(sb, typeNamesAppended, "transform");
         }
         
-        if (TemplateSequenceModel.class.isAssignableFrom(cl)) {
+        if (TemplateSequenceModel.class.isAssignableFrom(cl) || _CoreAPI.isLazilyGeneratedSequenceModel(cl)) {
             appendTypeName(sb, typeNamesAppended, "sequence");
         } else if (TemplateCollectionModel.class.isAssignableFrom(cl)) {
             appendTypeName(sb, typeNamesAppended,
diff --git a/src/main/resources/freemarker/included.html b/src/main/resources/freemarker/included.html
new file mode 100644
index 0000000..36c9254
--- /dev/null
+++ b/src/main/resources/freemarker/included.html
@@ -0,0 +1,9 @@
+<div class="code">
+<pre><code>SCRIPT_DIR=&quot;\
+  $(\
+    cd &quot;$(dirname &quot;${BASH_SRC_DIR[0]}&quot;)&quot; \
+    &gt;/dev/null 2&gt;&amp;1 \
+    &amp;&amp; pwd\
+  )&quot;
+SCRIPT_NAME=$(basename $0)</code></pre>
+</div>
\ No newline at end of file
diff --git a/src/test/java/freemarker/core/LazilyGeneratedSeqTargetSupportInBuiltinsTest.java b/src/test/java/freemarker/core/LazilyGeneratedSeqTest.java
similarity index 87%
rename from src/test/java/freemarker/core/LazilyGeneratedSeqTargetSupportInBuiltinsTest.java
rename to src/test/java/freemarker/core/LazilyGeneratedSeqTest.java
index 36af95f..b4618aa 100644
--- a/src/test/java/freemarker/core/LazilyGeneratedSeqTargetSupportInBuiltinsTest.java
+++ b/src/test/java/freemarker/core/LazilyGeneratedSeqTest.java
@@ -38,9 +38,31 @@ import freemarker.template.TemplateSequenceModel;
 import freemarker.test.TemplateTest;
 
 /**
- * Tests built-ins that are support getting {@link LazilyGeneratedSequenceModel} as their LHO input.
+ * Tests operators and built-ins that are support getting {@link LazilyGeneratedSequenceModel} as their operands.
  */
-public class LazilyGeneratedSeqTargetSupportInBuiltinsTest extends TemplateTest {
+public class LazilyGeneratedSeqTest extends TemplateTest {
+
+    @Test
+    public void dynamicIndexTest() throws Exception {
+        assertErrorContains("${coll?map(it -> it)['x']}",
+                "hash", "evaluated to a sequence");
+
+        assertOutput("${coll?map(it -> it)[0]}",
+                "[iterator][hasNext][next]1");
+        assertOutput("${coll?map(it -> it)[1]}",
+                "[iterator][hasNext][next][hasNext][next]2");
+        assertOutput("${coll?map(it -> it)[2]}",
+                "[iterator][hasNext][next][hasNext][next][hasNext][next]3");
+        assertOutput("${coll?map(it -> it)[3]!'missing'}",
+                "[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]missing");
+        assertOutput("${coll?filter(it -> it % 2 == 0)[0]}",
+                "[iterator][hasNext][next][hasNext][next]2");
+        assertOutput("${coll?filter(it -> it > 3)[0]!'missing'}",
+                "[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]missing");
+
+        assertOutput("${collLong?map(it -> it)[1 .. 2]?join(', ')}",
+                "[iterator][hasNext][next][hasNext][next][hasNext][next]2, 3");
+    }
 
     @Test
     public void sizeBasicsTest() throws Exception {
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/range-lazy.ftl b/src/test/resources/freemarker/test/templatesuite/templates/range-lazy.ftl
new file mode 100644
index 0000000..4fe96e1
--- /dev/null
+++ b/src/test/resources/freemarker/test/templatesuite/templates/range-lazy.ftl
@@ -0,0 +1,58 @@
+<#assign s="012">
+<#assign seq=[0, 1, 2]>
+
+<#list ['s', 'seq', 'seq?map(it -> it)', 'seq?filter(it -> true)'] as sliced>
+  <#-- Copy loop var to namespace var: -->
+  <#assign sliced = sliced>
+
+  <@assertSliceEquals '2', '2..' />
+  <@assertSliceFails '2..3' />
+  <@assertSliceEquals '2', '2..!3' />
+
+  <@assertSliceEquals '', '3..' />
+  <@assertSliceEquals '', '3..*1' />
+  <@assertSliceFails '3..*-1' />
+  <@assertSliceFails '3..3' />
+
+  <@assertSliceFails '4..' />
+  <@assertSliceFails '4..*1' />
+  <@assertSliceFails '4..*-1' />
+
+  <@assertSliceEquals '', '1..*0' />
+  <@assertSliceEquals '', '3..*0' />
+  <@assertSliceEquals '', '4..*0' />
+
+  <@assertSliceEquals '0', '0..*-1' />
+  <@assertSliceEquals '0', '0..*-2' />
+  <@assertSliceEquals '1', '1..*-1' />
+  <@assertSliceEquals '2', '2..*-1' />
+  <#if sliced != 's'>
+    <@assertSliceEquals '10', '1..*-2' />
+    <@assertSliceEquals '10', '1..*-3' />
+    <@assertSliceEquals '21', '2..*-2' />
+    <@assertSliceEquals '210', '2..*-3' />
+  <#else>
+    <@assertSliceFails '1..*-2' />
+    <@assertSliceFails '1..*-3' />
+    <@assertSliceFails '2..*-2' />
+  </#if>
+</#list>
+
+<#macro assertSliceEquals expected range>
+  <@assertJoinedEquals expected, ('${sliced}[${range}]')?eval />
+</#macro>
+
+<#macro assertSliceFails range>
+  <@assertFails><@consume ('${sliced}[${range}]')?eval /></@>
+</#macro>
+
+<#macro assertJoinedEquals expected actual>
+  <#local actualAsString = actual?isEnumerable?then(actual?join(''), actual)>
+  <@assertEquals expected=expected actual=actualAsString />
+</#macro>
+
+<#macro consume exp>
+  <#if exp?isEnumerable>
+    <#list exp as _></#list>
+  </#if>
+</#macro>
diff --git a/src/test/resources/freemarker/test/templatesuite/testcases.xml b/src/test/resources/freemarker/test/templatesuite/testcases.xml
index d514a2d..95cba52 100644
--- a/src/test/resources/freemarker/test/templatesuite/testcases.xml
+++ b/src/test/resources/freemarker/test/templatesuite/testcases.xml
@@ -18,7 +18,7 @@
   under the License.
 -->
 
-<!DOCTYPE testcases [
+<!DOCTYPE testCases [
   <!ELEMENT testCases (setting?, testCase*)>
   <!ELEMENT testCase (setting?)>
      <!ATTLIST testCase 
@@ -174,6 +174,9 @@
    <testCase name="range-ici-2.3.21" noOutput="true">
       <setting incompatible_improvements="2.3.21, max"/>
    </testCase>
+   <testCase name="range-lazy" noOutput="true">
+      <setting incompatible_improvements="2.3.22"/>
+   </testCase>
    <testCase name="recover" />
    <testCase name="root" />
    <testCase name="setting" noOutput="true" />


[freemarker] 03/03: exp[rangeExp] now returns sequence or lazily generated sequence depending on what the consumer supports. Also added size bypassing through exp[rangeExp] (like in seq?map(f)[10..]?size no seq element will be consumed, as the size can be calculated without that).

Posted by dd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch 2.3-gae
in repository https://gitbox.apache.org/repos/asf/freemarker.git

commit a2ed41d561484bdcd206df689b9809ae273a7680
Author: ddekany <dd...@apache.org>
AuthorDate: Sun May 26 00:44:16 2019 +0200

    exp[rangeExp] now returns sequence or lazily generated sequence depending on what the consumer supports. Also added size bypassing through exp[rangeExp] (like in seq?map(f)[10..]?size no seq element will be consumed, as the size can be calculated without that).
---
 src/main/java/freemarker/core/DynamicKeyName.java  | 145 +++++++++++++--------
 .../core/LazilyGeneratedSequenceModelWithSize.java |  41 ++++++
 .../freemarker/core/LazilyGeneratedSeqTest.java    |  47 ++++++-
 3 files changed, 177 insertions(+), 56 deletions(-)

diff --git a/src/main/java/freemarker/core/DynamicKeyName.java b/src/main/java/freemarker/core/DynamicKeyName.java
index 15ff96b..7b57983 100644
--- a/src/main/java/freemarker/core/DynamicKeyName.java
+++ b/src/main/java/freemarker/core/DynamicKeyName.java
@@ -20,7 +20,9 @@
 package freemarker.core;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
 import freemarker.template.SimpleScalar;
 import freemarker.template.SimpleSequence;
@@ -46,6 +48,7 @@ final class DynamicKeyName extends Expression {
 
     private final Expression keyExpression;
     private final Expression target;
+    private boolean lazilyGeneratedResultEnabled;
 
     DynamicKeyName(Expression target, Expression keyExpression) {
         this.target = target; 
@@ -162,7 +165,7 @@ final class DynamicKeyName extends Expression {
 
     private TemplateModel dealWithRangeKey(TemplateModel targetModel, RangeModel range, Environment env)
     throws TemplateException {
-        // We can have 3 kind of left hand operands ("targets"): sequence, lazyily generated sequence, string
+        // We can have 3 kind of left hand operands ("targets"): sequence, lazily generated sequence, string
         final TemplateSequenceModel targetSeq;
         final LazilyGeneratedSequenceModel targetLazySeq;
         final String targetStr;
@@ -238,8 +241,11 @@ final class DynamicKeyName extends Expression {
                         ". ", "(Note that indices are 0-based).");
             }
         }
-        
-        final int resultSize; // Might will be UNKNOWN_RESULT_SIZE, when targetLazySeq != null
+
+        // Calculate resultSize. Note that:
+        // - It might will be UNKNOWN_RESULT_SIZE when targetLazySeq != null.
+        // - It might will be out-of-bounds if !targetSizeKnown (otherwise we validate if the range is correct)
+        final int resultSize;
         if (!rightUnbounded) {
             final int lastIdx = firstIdx + (rangeSize - 1) * step; // Note: lastIdx is inclusive
             if (lastIdx < 0) {
@@ -271,6 +277,9 @@ final class DynamicKeyName extends Expression {
             return emptyResult(targetSeq != null);
         }
         if (targetSeq != null) {
+            // In principle we should take lazilyGeneratedResultEnabled into account, but that wouldn't be backward
+            // compatible. For example, with lazily generated sequence result <#list xs[b..e] as x> would behave
+            // differently if xs is modified inside the #list nested content.
             ArrayList<TemplateModel> resultList = new ArrayList<TemplateModel>(resultSize);
             int srcIdx = firstIdx;
             for (int i = 0; i < resultSize; i++) {
@@ -280,8 +289,10 @@ final class DynamicKeyName extends Expression {
             // List items are already wrapped, so the wrapper will be null:
             return new SimpleSequence(resultList, null);
         } else if (targetLazySeq != null) {
+            // As a targetLazySeq can only occur if a new built-in like ?filter or ?map was used somewhere in the target
+            // expression, in this case we can return lazily generated sequence without breaking backward compatibility.
             if (step == 1) {
-                return getStep1RangeFromIterator(targetLazySeq.iterator(), range, resultSize);
+                return getStep1RangeFromIterator(targetLazySeq.iterator(), range, resultSize, targetSizeKnown);
             } else if (step == -1) {
                 return getStepMinus1RangeFromIterator(targetLazySeq.iterator(), range, resultSize);
             } else {
@@ -308,55 +319,88 @@ final class DynamicKeyName extends Expression {
         }
     }
 
-    private TemplateModel getStep1RangeFromIterator(final TemplateModelIterator targetIter, final RangeModel range, int resultSize)
+    private TemplateModel getStep1RangeFromIterator(
+            final TemplateModelIterator targetIter, final RangeModel range, int resultSize, boolean targetSizeKnown)
             throws TemplateModelException {
         final int firstIdx = range.getBegining();
         final int lastIdx = firstIdx + (range.size() - 1); // Note: meaningless if the range is right unbounded
         final boolean rightAdaptive = range.isRightAdaptive();
         final boolean rightUnbounded = range.isRightUnbounded();
-        return new LazilyGeneratedSequenceModel(new TemplateModelIterator() {
-            private boolean elementsBeforeFirsIndexWereSkipped;
-            private int nextIdx;
-
-            public TemplateModel next() throws TemplateModelException {
-                ensureElementsBeforeFirstIndexWereSkipped();
-                if (!rightUnbounded && nextIdx > lastIdx) {
-                    throw new _TemplateModelException(
-                            "Iterator has no more elements (at index ", Integer.valueOf(nextIdx), ")");
+        if (lazilyGeneratedResultEnabled) {
+            TemplateModelIterator iterator = new TemplateModelIterator() {
+                private boolean elementsBeforeFirsIndexWereSkipped;
+                private int nextIdx;
+
+                public TemplateModel next() throws TemplateModelException {
+                    ensureElementsBeforeFirstIndexWereSkipped();
+                    if (!rightUnbounded && nextIdx > lastIdx) {
+                        // We fail because the consumer of this iterator hasn't used hasNext() properly.
+                        throw new _TemplateModelException(
+                                "Iterator has no more elements (at index ", Integer.valueOf(nextIdx), ")");
+                    }
+                    if (!rightAdaptive && !targetIter.hasNext()) {
+                        // We fail because the range was wrong, not because of the consumer of this iterator .
+                        throw newRangeEndOutOfBoundsException(nextIdx, lastIdx);
+                    }
+                    TemplateModel result = targetIter.next();
+                    nextIdx++;
+                    return result;
                 }
-                if (!rightAdaptive && !targetIter.hasNext()) {
-                    // We fail because the range was wrong, not because this iterator was over-consumed.
-                    throw new _TemplateModelException(keyExpression,
-                            "Range end index ", Integer.valueOf(lastIdx), " is out of bounds, as sliced sequence " +
-                            "only has ", nextIdx, " elements.");
+
+                public boolean hasNext() throws TemplateModelException {
+                    ensureElementsBeforeFirstIndexWereSkipped();
+                    return (rightUnbounded || nextIdx <= lastIdx) && (!rightAdaptive || targetIter.hasNext());
                 }
-                TemplateModel result = targetIter.next();
-                nextIdx++;
-                return result;
-            }
 
-            public boolean hasNext() throws TemplateModelException {
-                ensureElementsBeforeFirstIndexWereSkipped();
-                return (rightUnbounded || nextIdx <= lastIdx) && (!rightAdaptive || targetIter.hasNext());
-            }
+                public void ensureElementsBeforeFirstIndexWereSkipped() throws TemplateModelException {
+                    if (elementsBeforeFirsIndexWereSkipped) {
+                        return;
+                    }
 
-            public void ensureElementsBeforeFirstIndexWereSkipped() throws TemplateModelException {
-                if (elementsBeforeFirsIndexWereSkipped) {
-                    return;
+                    skipElementsBeforeFirstIndex(targetIter, firstIdx);
+                    nextIdx = firstIdx;
+                    elementsBeforeFirsIndexWereSkipped = true;
                 }
-
-                while (nextIdx < firstIdx) {
-                    if (!targetIter.hasNext()) {
-                        throw new _TemplateModelException(keyExpression,
-                                "Range start index ", Integer.valueOf(firstIdx), " is out of bounds, as the sliced " +
-                                "sequence only has ", nextIdx, " elements.");
+            };
+            return resultSize != UNKNOWN_RESULT_SIZE && targetSizeKnown  // targetSizeKnown => range end was validated
+                    ? new LazilyGeneratedSequenceModelWithSize(iterator, resultSize)
+                    : new LazilyGeneratedSequenceModel(iterator);
+        } else { // !lazilyGeneratedResultEnabled
+            List<TemplateModel> resultList = resultSize != UNKNOWN_RESULT_SIZE
+                    ? new ArrayList<TemplateModel>(resultSize)
+                    : new ArrayList<TemplateModel>();
+            skipElementsBeforeFirstIndex(targetIter, firstIdx);
+            collectElements: for (int nextIdx = firstIdx; rightUnbounded || nextIdx <= lastIdx; nextIdx++) {
+                if (!targetIter.hasNext()) {
+                    if (!rightAdaptive) {
+                        throw newRangeEndOutOfBoundsException(nextIdx, lastIdx);
                     }
-                    targetIter.next();
-                    nextIdx++;
+                    break collectElements;
                 }
-                elementsBeforeFirsIndexWereSkipped = true;
+                resultList.add(targetIter.next());
             }
-        });
+            // List items are already wrapped, so the wrapper will be null:
+            return new SimpleSequence(resultList, null);
+        }
+    }
+
+    private void skipElementsBeforeFirstIndex(TemplateModelIterator targetIter, int firstIdx) throws TemplateModelException {
+        int nextIdx = 0;
+        while (nextIdx < firstIdx) {
+            if (!targetIter.hasNext()) {
+                throw new _TemplateModelException(keyExpression,
+                        "Range start index ", Integer.valueOf(firstIdx),
+                        " is out of bounds, as the sliced sequence only has ", nextIdx, " elements.");
+            }
+            targetIter.next();
+            nextIdx++;
+        }
+    }
+
+    private _TemplateModelException newRangeEndOutOfBoundsException(int nextIdx, int lastIdx) {
+        return new _TemplateModelException(keyExpression,
+                "Range end index ", Integer.valueOf(lastIdx), " is out of bounds, as sliced sequence " +
+                "only has ", nextIdx, " elements.");
     }
 
     // Because the order has to be reversed, we have to "buffer" the stream in this case.
@@ -383,21 +427,7 @@ final class DynamicKeyName extends Expression {
                     "Range top index " + highIndex + " (0-based) is outside the sliced sequence of length " +
                     srcIdx + ".");
         }
-        return new LazilyGeneratedSequenceModel(new TemplateModelIterator() {
-            private int nextIndex;
-
-            public TemplateModel next() throws TemplateModelException {
-                try {
-                    return resultElements[nextIndex++];
-                } catch (IndexOutOfBoundsException e) {
-                    throw new TemplateModelException("There are no more elements in the iterator");
-                }
-            }
-
-            public boolean hasNext() throws TemplateModelException {
-                return nextIndex < resultElements.length;
-            }
-        });
+        return new SimpleSequence(Arrays.asList(resultElements), null);
     }
 
     private TemplateModel emptyResult(boolean seq) {
@@ -409,6 +439,11 @@ final class DynamicKeyName extends Expression {
     }
 
     @Override
+    void enableLazilyGeneratedResult() {
+        lazilyGeneratedResultEnabled = true;
+    }
+
+    @Override
     public String getCanonicalForm() {
         return target.getCanonicalForm() 
                + "[" 
diff --git a/src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java b/src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java
new file mode 100644
index 0000000..7faab2e
--- /dev/null
+++ b/src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java
@@ -0,0 +1,41 @@
+/*
+ * 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 freemarker.core;
+
+import freemarker.template.TemplateCollectionModelEx;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateModelIterator;
+
+class LazilyGeneratedSequenceModelWithSize extends LazilyGeneratedSequenceModel implements TemplateCollectionModelEx {
+    private final int size;
+
+    LazilyGeneratedSequenceModelWithSize(TemplateModelIterator iterator, int size) {
+        super(iterator);
+        this.size = size;
+    }
+
+    public int size() throws TemplateModelException {
+        return size;
+    }
+
+    public boolean isEmpty() {
+        return size == 0;
+    }
+}
diff --git a/src/test/java/freemarker/core/LazilyGeneratedSeqTest.java b/src/test/java/freemarker/core/LazilyGeneratedSeqTest.java
index b4618aa..0886f05 100644
--- a/src/test/java/freemarker/core/LazilyGeneratedSeqTest.java
+++ b/src/test/java/freemarker/core/LazilyGeneratedSeqTest.java
@@ -172,7 +172,51 @@ public class LazilyGeneratedSeqTest extends TemplateTest {
                 "[iterator][hasNext][next][hasNext][next]2");
     }
 
-
+    @Test
+    public void rangeOperatorTest() throws Exception {
+        assertErrorContains("${coll[1..2]?join(', ')}", "sequence", "collection");
+
+        assertOutput("${seq[1..2]?first}", "[size][get 1][get 2]2");
+        assertOutput("${seq[1..]?first}",  "[size][get 1][get 2]2");
+        assertOutput("${seq[2..1]?first}",  "[size][get 2][get 1]3");
+
+        assertOutput("${seqLong[1..3]?first}", "[size][get 1][get 2][get 3]2");
+        assertOutput("${seqLong[1..]?first}",  "[size][get 1][get 2][get 3][get 4][get 5]2");
+        assertOutput("${seqLong[3..1]?first}",  "[size][get 3][get 2][get 1]4");
+
+        assertOutput("${seq?map(x->x)[1..2]?first}", "[size][size][get 0][get 1]2");
+        assertOutput("${seq?map(x->x)[1..]?first}",  "[size][size][get 0][get 1]2");
+        // Why 2 [size]-s above: 1st to validate range. 2nd for the 1st hasNext call on the iterator.
+        assertOutput("${seq?map(x->x)[2..1]?first}",  "[size][size][get 0][get 1][get 2]3");
+
+        assertOutput("${seqLong?map(x->x)[1..3]?first}", "[size][size][get 0][get 1]2");
+        assertOutput("${seqLong?map(x->x)[1..]?first}",  "[size][size][get 0][get 1]2");
+        assertOutput("${seqLong?map(x->x)[3..1]?first}",  "[size][size][get 0][get 1][get 2][get 3]4");
+
+        assertOutput("${seq?filter(x->true)[1..2]?first}", "[size][get 0][get 1]2");
+        assertOutput("${seq?filter(x->true)[1..]?first}",  "[size][get 0][get 1]2");
+        assertOutput("${seq?filter(x->true)[2..1]?first}",  "[size][get 0][get 1][get 2]3");
+
+        assertOutput("${seqLong?filter(x->true)[1..3]?first}", "[size][get 0][get 1]2");
+        assertOutput("${seqLong?filter(x->true)[1..]?first}",  "[size][get 0][get 1]2");
+        assertOutput("${seqLong?filter(x->true)[3..1]?first}",  "[size][get 0][get 1][get 2][get 3]4");
+
+        assertOutput("${seq[1..2][0..1]?first}", "[size][get 1][get 2]2");
+        assertOutput("${seq?map(x->x)[1..2][0..1]?first}", "[size][size][get 0][get 1]2");
+        assertOutput("${seq?filter(x->true)[1..2][0..1]?first}", "[size][get 0][get 1]2");
+
+        assertOutput("<#list seqLong?filter(x->true)[1..3] as it>${it}</#list>",
+                "[size][get 0][get 1]2[get 2]3[get 3]4");
+        assertOutput("<#list seqLong[1..3] as it>${it}</#list>",
+                "[size][get 1][get 2][get 3]234");
+
+        assertOutput("${seq?map(x->x)[1..2]?size}", "[size]2");
+        assertOutput("${seq?filter(x->true)[1..2]?size}", "[size][get 0][get 1][get 2]2");
+        assertOutput("${seqLong?map(x->x)[2..]?size}", "[size]4");
+        assertOutput("${seqLong?filter(x->true)[2..]?size}", "[size][get 0][get 1][get 2][get 3][get 4][get 5]4");
+        assertOutput("${seqLong?map(x->x)[2..*3]?size}", "[size]3");
+        assertOutput("${seqLong?filter(x->true)[2..*3]?size}", "[size][get 0][get 1][get 2][get 3][get 4]3");
+    }
 
     @Override
     protected Configuration createConfiguration() throws Exception {
@@ -180,6 +224,7 @@ public class LazilyGeneratedSeqTest extends TemplateTest {
         cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_29);
         cfg.setBooleanFormat("c");
         cfg.setSharedVariable("seq", new MonitoredTemplateSequenceModel(1, 2, 3));
+        cfg.setSharedVariable("seqLong", new MonitoredTemplateSequenceModel(1, 2, 3, 4, 5, 6));
         cfg.setSharedVariable("coll", new MonitoredTemplateCollectionModel(1, 2, 3));
         cfg.setSharedVariable("collLong", new MonitoredTemplateCollectionModel(1, 2, 3, 4, 5, 6));
         cfg.setSharedVariable("collEx", new MonitoredTemplateCollectionModelEx(1, 2, 3));


[freemarker] 02/03: Generalized/simplified the way it's decided if an expression can produce or consume lazily generated values.

Posted by dd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch 2.3-gae
in repository https://gitbox.apache.org/repos/asf/freemarker.git

commit 1f25c8b538d9c5ecc5fb4e95f3b65cd55b6a60c7
Author: ddekany <dd...@apache.org>
AuthorDate: Sat May 25 14:59:36 2019 +0200

    Generalized/simplified the way it's decided if an expression can produce or consume lazily generated values.
---
 src/main/java/freemarker/core/BuiltIn.java         | 13 +++---
 .../freemarker/core/BuiltInsForMultipleTypes.java  |  2 +-
 .../java/freemarker/core/BuiltInsForSequences.java | 46 +++++++++-------------
 src/main/java/freemarker/core/DynamicKeyName.java  |  6 +--
 src/main/java/freemarker/core/Expression.java      | 18 +++++++++
 src/main/java/freemarker/core/IteratorBlock.java   | 10 +----
 .../core/LazilyGeneratedSequenceModel.java         |  6 ++-
 .../freemarker/core/ParentheticalExpression.java   |  7 +++-
 .../core/SingleIterationCollectionModel.java       |  2 +-
 9 files changed, 55 insertions(+), 55 deletions(-)

diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java
index 9e3dd3c..57c3313 100644
--- a/src/main/java/freemarker/core/BuiltIn.java
+++ b/src/main/java/freemarker/core/BuiltIn.java
@@ -386,18 +386,15 @@ abstract class BuiltIn extends Expression implements Cloneable {
             throw new InternalError();
         }
         bi.key = key;
-        bi.target = target;
-        if (bi.isLazilyGeneratedSequenceModelTargetSupported()) {
-            Expression cleanedTarget = MiscUtil.peelParentheses(target);
-            if (cleanedTarget instanceof BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) {
-                ((BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) cleanedTarget)
-                        .setLazyResultGenerationAllowed(true);
-            }
+        if (bi.isLazilyGeneratedTargetResultSupported()) {
+            target.enableLazilyGeneratedResult();
         }
+        bi.target = target;
         return bi;
     }
 
-    protected boolean isLazilyGeneratedSequenceModelTargetSupported() {
+    /** If the built-in supports a lazily generated value as its left operand (the target). */
+    protected boolean isLazilyGeneratedTargetResultSupported() {
         return false;
     }
 
diff --git a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
index b690cf5..c8a0922 100644
--- a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
+++ b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
@@ -485,7 +485,7 @@ class BuiltInsForMultipleTypes {
     static class sizeBI extends BuiltIn {
 
         @Override
-        protected boolean isLazilyGeneratedSequenceModelTargetSupported() {
+        protected boolean isLazilyGeneratedTargetResultSupported() {
             return true;
         }
 
diff --git a/src/main/java/freemarker/core/BuiltInsForSequences.java b/src/main/java/freemarker/core/BuiltInsForSequences.java
index b4acd16..ba40615 100644
--- a/src/main/java/freemarker/core/BuiltInsForSequences.java
+++ b/src/main/java/freemarker/core/BuiltInsForSequences.java
@@ -144,7 +144,7 @@ class BuiltInsForSequences {
     static class firstBI extends BuiltIn {
 
         @Override
-        protected boolean isLazilyGeneratedSequenceModelTargetSupported() {
+        protected boolean isLazilyGeneratedTargetResultSupported() {
             return true;
         }
 
@@ -185,7 +185,7 @@ class BuiltInsForSequences {
     static class joinBI extends BuiltIn {
 
         @Override
-        protected boolean isLazilyGeneratedSequenceModelTargetSupported() {
+        protected boolean isLazilyGeneratedTargetResultSupported() {
             return true;
         }
 
@@ -302,7 +302,7 @@ class BuiltInsForSequences {
     static class seq_containsBI extends BuiltIn {
 
         @Override
-        protected boolean isLazilyGeneratedSequenceModelTargetSupported() {
+        protected boolean isLazilyGeneratedTargetResultSupported() {
             return true;
         }
 
@@ -374,7 +374,7 @@ class BuiltInsForSequences {
     static class seq_index_ofBI extends BuiltIn {
 
         @Override
-        protected boolean isLazilyGeneratedSequenceModelTargetSupported() {
+        protected boolean isLazilyGeneratedTargetResultSupported() {
             return true;
         }
 
@@ -915,7 +915,7 @@ class BuiltInsForSequences {
         }
 
         @Override
-        protected boolean isLazilyGeneratedSequenceModelTargetSupported() {
+        protected boolean isLazilyGeneratedTargetResultSupported() {
             return true;
         }
 
@@ -989,7 +989,7 @@ class BuiltInsForSequences {
 
         private Expression elementTransformerExp;
         private ElementTransformer precreatedElementTransformer;
-        private boolean lazyResultGenerationAllowed;
+        private boolean lazilyGeneratedResultEnabled;
 
         @Override
         void bindToParameters(List<Expression> parameters, Token openParen, Token closeParen) throws ParseException {
@@ -1007,33 +1007,23 @@ class BuiltInsForSequences {
         }
 
         @Override
-        protected boolean isLocalLambdaParameterSupported() {
+        protected final boolean isLocalLambdaParameterSupported() {
             return true;
         }
 
         @Override
-        protected boolean isLazilyGeneratedSequenceModelTargetSupported() {
-            return true;
+        final void enableLazilyGeneratedResult() {
+            this.lazilyGeneratedResultEnabled = true;
         }
 
-        final boolean isLazyResultGenerationAllowed() {
-            return lazyResultGenerationAllowed;
+        /** Tells if {@link #enableLazilyGeneratedResult()} was called. */
+        protected final boolean isLazilyGeneratedResultEnabled() {
+            return lazilyGeneratedResultEnabled;
         }
 
-        /**
-         * Used to allow generating the result collection or sequence elements on an as-needed basis, similarly as
-         * Java 8 Stream intermediate operations do it. This is initially {@code false}. The containing expression or
-         * directive sets it to {@code true} if it can ensure that:
-         * <ul>
-         *   <li>The returned {@link TemplateCollectionModel} is traversed only once, more specifically,
-         *       {@link TemplateCollectionModel#iterator()} is called only once.
-         *   <li>When the methods of the collection or iterator are called, the context provided by
-         *       the {@link Environment} (such as the local context stack) is similar to the context from where the
-         *       built-in was called. This is required as lambda expression are {@link LocalLambdaExpression}-s.
-         * </ul>
-         */
-        void setLazyResultGenerationAllowed(boolean lazyResultGenerationAllowed) {
-            this.lazyResultGenerationAllowed = lazyResultGenerationAllowed;
+        @Override
+        protected final boolean isLazilyGeneratedTargetResultSupported() {
+            return true;
         }
 
         protected List<Expression> getArgumentsAsList() {
@@ -1086,7 +1076,7 @@ class BuiltInsForSequences {
         private TemplateModelIterator getTemplateModelIterator(Environment env, TemplateModel model) throws TemplateModelException,
                 NonSequenceOrCollectionException, InvalidReferenceException {
             if (model instanceof TemplateCollectionModel) {
-                return isLazyResultGenerationAllowed()
+                return isLazilyGeneratedResultEnabled()
                         ? new LazyCollectionTemplateModelIterator((TemplateCollectionModel) model)
                         : ((TemplateCollectionModel) model).iterator();
             } else if (model instanceof TemplateSequenceModel) {
@@ -1176,7 +1166,7 @@ class BuiltInsForSequences {
                 final TemplateModelIterator lhoIterator, final TemplateModel lho,
                 final ElementTransformer elementTransformer,
                 final Environment env) throws TemplateException {
-            if (!isLazyResultGenerationAllowed()) {
+            if (!isLazilyGeneratedResultEnabled()) {
                 List<TemplateModel> resultList = new ArrayList<TemplateModel>();
                 while (lhoIterator.hasNext()) {
                     TemplateModel element = lhoIterator.next();
@@ -1262,7 +1252,7 @@ class BuiltInsForSequences {
         protected TemplateModel calculateResult(
                 final TemplateModelIterator lhoIterator, TemplateModel lho, final ElementTransformer elementTransformer,
                 final Environment env) throws TemplateException {
-            if (!isLazyResultGenerationAllowed()) {
+            if (!isLazilyGeneratedResultEnabled()) {
                 List<TemplateModel> resultList = new ArrayList<TemplateModel>();
                 while (lhoIterator.hasNext()) {
                     resultList.add(fetchAndMapNextElement(lhoIterator, elementTransformer, env));
diff --git a/src/main/java/freemarker/core/DynamicKeyName.java b/src/main/java/freemarker/core/DynamicKeyName.java
index c01d0ef..15ff96b 100644
--- a/src/main/java/freemarker/core/DynamicKeyName.java
+++ b/src/main/java/freemarker/core/DynamicKeyName.java
@@ -51,11 +51,7 @@ final class DynamicKeyName extends Expression {
         this.target = target; 
         this.keyExpression = keyExpression;
 
-        Expression cleanedTarget = MiscUtil.peelParentheses(target);
-        if (cleanedTarget instanceof BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) {
-            ((BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) cleanedTarget)
-                    .setLazyResultGenerationAllowed(true);
-        }
+        target.enableLazilyGeneratedResult();
     }
 
     @Override
diff --git a/src/main/java/freemarker/core/Expression.java b/src/main/java/freemarker/core/Expression.java
index 0c3575c..5a69095 100644
--- a/src/main/java/freemarker/core/Expression.java
+++ b/src/main/java/freemarker/core/Expression.java
@@ -77,6 +77,24 @@ abstract public class Expression extends TemplateObject {
     public final TemplateModel getAsTemplateModel(Environment env) throws TemplateException {
         return eval(env);
     }
+
+    /**
+     * Allows generating the result collection or sequence elements (or maybe others in future) on an as-needed basis,
+     * similarly as Java 8 Stream intermediate operations do it. The default implementation in {@link Expression}
+     * does nothing, as most expressions can't create such result. The containing expression or directive calls
+     * this if it can ensure that:
+     * <ul>
+     *   <li>If the returned value is a {@link TemplateCollectionModel}, then it's traversed at most once, more
+     *       specifically, {@link TemplateCollectionModel#iterator()} is called at most once.
+     *   <li>When the methods of the collection or iterator are called, the context provided by
+     *       the {@link Environment} (such as the local context stack) is similar to the context from where this
+     *       expression was called. This is required as lazily generated results are allowed to be based on
+     *       {@link LocalLambdaExpression}-s.
+     * </ul>
+     */
+    void enableLazilyGeneratedResult() {
+        // Has no effect by default
+    }
     
     final TemplateModel eval(Environment env) throws TemplateException {
         try {
diff --git a/src/main/java/freemarker/core/IteratorBlock.java b/src/main/java/freemarker/core/IteratorBlock.java
index c734cbe..aaaaf64 100644
--- a/src/main/java/freemarker/core/IteratorBlock.java
+++ b/src/main/java/freemarker/core/IteratorBlock.java
@@ -48,7 +48,6 @@ final class IteratorBlock extends TemplateElement {
     private final String loopVar2Name;
     private final boolean hashListing;
     private final boolean forEach;
-    private final boolean fetchElementsOutsideLoopVarContext;
 
     /**
      * @param listedExp
@@ -83,14 +82,7 @@ final class IteratorBlock extends TemplateElement {
         this.hashListing = hashListing;
         this.forEach = forEach;
 
-        Expression cleanedListExp = MiscUtil.peelParentheses(listedExp);
-        if (cleanedListExp instanceof BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) {
-            ((BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) cleanedListExp)
-                    .setLazyResultGenerationAllowed(true);
-            fetchElementsOutsideLoopVarContext = true;
-        } else {
-            fetchElementsOutsideLoopVarContext = false;
-        }
+        listedExp.enableLazilyGeneratedResult();
     }
     
     boolean isHashListing() {
diff --git a/src/main/java/freemarker/core/LazilyGeneratedSequenceModel.java b/src/main/java/freemarker/core/LazilyGeneratedSequenceModel.java
index a0737f1..26e210a 100644
--- a/src/main/java/freemarker/core/LazilyGeneratedSequenceModel.java
+++ b/src/main/java/freemarker/core/LazilyGeneratedSequenceModel.java
@@ -26,10 +26,12 @@ import freemarker.template.TemplateSequenceModel;
 /**
  * Same as {@link SingleIterationCollectionModel}, but marks the value as something that's in principle a
  * {@link TemplateSequenceModel}, but to allow lazy result generation a {@link CollectionModel} is used internally.
- * This is an optimization that we do where we consider it to be transparent enough for the user.
+ * This is an optimization that we do where we consider it to be transparent enough for the user. An operator or
+ * built-in should only ever receive a {@link LazilyGeneratedSequenceModel} if it has explicitly allowed its
+ * input expression to return such value via calling {@link Expression#enableLazilyGeneratedResult()}.
  */
 class LazilyGeneratedSequenceModel extends SingleIterationCollectionModel {
-    public LazilyGeneratedSequenceModel(TemplateModelIterator iterator) {
+    LazilyGeneratedSequenceModel(TemplateModelIterator iterator) {
         super(iterator);
     }
 }
diff --git a/src/main/java/freemarker/core/ParentheticalExpression.java b/src/main/java/freemarker/core/ParentheticalExpression.java
index f63e0b6..07f5d18 100644
--- a/src/main/java/freemarker/core/ParentheticalExpression.java
+++ b/src/main/java/freemarker/core/ParentheticalExpression.java
@@ -60,6 +60,11 @@ final class ParentheticalExpression extends Expression {
     }
 
     @Override
+    void enableLazilyGeneratedResult() {
+        nested.enableLazilyGeneratedResult();
+    }
+
+    @Override
     protected Expression deepCloneWithIdentifierReplaced_inner(
             String replacedIdentifier, Expression replacement, ReplacemenetState replacementState) {
         return new ParentheticalExpression(
@@ -82,5 +87,5 @@ final class ParentheticalExpression extends Expression {
         if (idx != 0) throw new IndexOutOfBoundsException();
         return ParameterRole.ENCLOSED_OPERAND;
     }
-    
+
 }
diff --git a/src/main/java/freemarker/core/SingleIterationCollectionModel.java b/src/main/java/freemarker/core/SingleIterationCollectionModel.java
index 292bcd3..56efe93 100644
--- a/src/main/java/freemarker/core/SingleIterationCollectionModel.java
+++ b/src/main/java/freemarker/core/SingleIterationCollectionModel.java
@@ -34,7 +34,7 @@ import freemarker.template.utility.NullArgumentException;
 class SingleIterationCollectionModel implements TemplateCollectionModel {
     private TemplateModelIterator iterator;
 
-    public SingleIterationCollectionModel(TemplateModelIterator iterator) {
+    SingleIterationCollectionModel(TemplateModelIterator iterator) {
         NullArgumentException.check(iterator);
         this.iterator = iterator;
     }