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/06/23 23:25:53 UTC

[freemarker] branch 2.3-gae updated: Now operations that require a sequence will only accept a lazily generated collection (typically returned by ?filter or ?map) if it was explicitly marked as a sequence (via LazilyGeneratedCollectionModel.isSequence()). Thus, applying operations like ?filter or ?map on a non-sequence enumerable value won't allow sequence-only operations. This was made this strict as if a huge collection was passed to the template as Iterator, an implicit conversion to sequence can consume too much memory. [...]

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


The following commit(s) were added to refs/heads/2.3-gae by this push:
     new e05bd9a  Now operations that require a sequence will only accept a lazily generated collection (typically returned by ?filter or ?map) if it was explicitly marked as a sequence (via LazilyGeneratedCollectionModel.isSequence()). Thus, applying  operations like ?filter or ?map on a non-sequence enumerable value won't allow sequence-only operations. This was made this strict as if a huge collection was passed to the template as Iterator, an implicit conversion to sequence can consum [...]
e05bd9a is described below

commit e05bd9a699eadce4a0b50de950b5da8ae0cf0f11
Author: ddekany <dd...@apache.org>
AuthorDate: Sun Jun 23 16:55:25 2019 +0200

    Now operations that require a sequence will only accept a lazily generated collection (typically returned by ?filter or ?map) if it was explicitly marked as a sequence (via LazilyGeneratedCollectionModel.isSequence()). Thus, applying  operations like ?filter or ?map on a non-sequence enumerable value won't allow sequence-only operations. This was made this strict as if a huge collection was passed to the template as Iterator, an implicit conversion to sequence can consume too much mem [...]
---
 src/main/java/freemarker/core/BuiltIn.java         |   7 +-
 .../freemarker/core/BuiltInsForMultipleTypes.java  |   7 +-
 .../java/freemarker/core/BuiltInsForSequences.java | 118 ++++++++++++++-------
 src/main/java/freemarker/core/DynamicKeyName.java  |  19 ++--
 .../core/LazilyGeneratedCollectionModel.java       |  80 ++++++++++++++
 ....java => LazilyGeneratedCollectionModelEx.java} |  16 ++-
 ...eratedCollectionModelWithAlreadyKnownSize.java} |  12 ++-
 ...eneratedCollectionModelWithSameSizeCollEx.java} |  16 +--
 ...lyGeneratedCollectionModelWithSameSizeSeq.java} |  17 +--
 ...lyGeneratedCollectionModelWithUnknownSize.java} |  20 ++--
 .../core/SingleIterationCollectionModel.java       |   4 +
 src/main/java/freemarker/core/_CoreAPI.java        |   5 +-
 src/main/java/freemarker/core/_MessageUtil.java    |  22 ++--
 .../freemarker/template/utility/ClassUtil.java     |   5 +-
 src/manual/en_US/book.xml                          |  26 +++++
 src/test/java/freemarker/core/FilterBiTest.java    |  25 +++++
 ...est.java => LazilyGeneratedCollectionTest.java} |  94 ++++++++++------
 src/test/java/freemarker/core/MapBiTest.java       |  19 ++++
 18 files changed, 382 insertions(+), 130 deletions(-)

diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java
index 57c3313..4e26f50 100644
--- a/src/main/java/freemarker/core/BuiltIn.java
+++ b/src/main/java/freemarker/core/BuiltIn.java
@@ -393,7 +393,12 @@ abstract class BuiltIn extends Expression implements Cloneable {
         return bi;
     }
 
-    /** If the built-in supports a lazily generated value as its left operand (the target). */
+    /**
+     * If the built-in supports a lazily generated value as its left operand (the target).
+     * Don't confuse this with what's allowed for result of the built-in itself; that's influenced by
+     * {@link Expression#enableLazilyGeneratedResult()} (and so
+     * {@link BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn#isLazilyGeneratedTargetResultSupported()}).
+     */
     protected boolean isLazilyGeneratedTargetResultSupported() {
         return false;
     }
diff --git a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
index c8a0922..34ff1f0 100644
--- a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
+++ b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
@@ -504,14 +504,15 @@ class BuiltInsForMultipleTypes {
                 size = ((TemplateCollectionModelEx) model).size();
             } else if (model instanceof TemplateHashModelEx) {
                 size = ((TemplateHashModelEx) model).size();
-            } else if (model instanceof LazilyGeneratedSequenceModel) {
+            } else if (model instanceof LazilyGeneratedCollectionModel
+                    && ((LazilyGeneratedCollectionModel) model).isSequence()) {
                 // While this is a TemplateCollectionModel, and thus ?size will be O(N), and N might be infinite,
                 // it's for the result of ?filter(predicate) or such. Those "officially" return a sequence. Returning a
-                // TemplateCollectionModel (a LazilyGeneratedSequenceModel more specifically) is a (mostly) transparent
+                // TemplateCollectionModel (a LazilyGeneratedCollectionModel to be exact) is a (mostly) transparent
                 // optimization to avoid creating the result sequence in memory, which would be unnecessary work for
                 // ?size. Creating that result sequence would be O(N) too, so the O(N) time complexity should be
                 // expected by the template author, and we just made that calculation less wasteful here.
-                TemplateModelIterator iterator = ((LazilyGeneratedSequenceModel) model).iterator();
+                TemplateModelIterator iterator = ((LazilyGeneratedCollectionModel) model).iterator();
                 int counter = 0;
                 countElements: while (iterator.hasNext()) {
                     counter++;
diff --git a/src/main/java/freemarker/core/BuiltInsForSequences.java b/src/main/java/freemarker/core/BuiltInsForSequences.java
index ba40615..a232025 100644
--- a/src/main/java/freemarker/core/BuiltInsForSequences.java
+++ b/src/main/java/freemarker/core/BuiltInsForSequences.java
@@ -855,6 +855,8 @@ class BuiltInsForSequences {
 
     static class sequenceBI extends BuiltIn {
 
+        private boolean lazilyGeneratedResultEnabled;
+
         @Override
         TemplateModel _eval(Environment env) throws TemplateException {
             TemplateModel model = target.eval(env);
@@ -867,17 +869,37 @@ class BuiltInsForSequences {
                 throw new NonSequenceOrCollectionException(target, model, env);
             }
             TemplateCollectionModel coll = (TemplateCollectionModel) model;
-            
-            SimpleSequence seq =
-                    coll instanceof TemplateCollectionModelEx
-                            ? new SimpleSequence(((TemplateCollectionModelEx) coll).size())
-                            : new SimpleSequence();
-            for (TemplateModelIterator iter = coll.iterator(); iter.hasNext(); ) {
-                seq.add(iter.next());
+
+            if (!lazilyGeneratedResultEnabled) {
+                SimpleSequence seq =
+                        coll instanceof TemplateCollectionModelEx
+                                ? new SimpleSequence(((TemplateCollectionModelEx) coll).size())
+                                : new SimpleSequence();
+                for (TemplateModelIterator iter = coll.iterator(); iter.hasNext(); ) {
+                    seq.add(iter.next());
+                }
+                return seq;
+            } else {
+                return coll instanceof LazilyGeneratedCollectionModel
+                        ? ((LazilyGeneratedCollectionModel) coll).withIsSequenceTrue()
+                        : coll instanceof TemplateCollectionModelEx
+                                ? new LazilyGeneratedCollectionModelWithSameSizeCollEx(
+                                        new LazyCollectionTemplateModelIterator(coll),
+                                        (TemplateCollectionModelEx) coll, true)
+                                : new LazilyGeneratedCollectionModelWithUnknownSize(
+                                        new LazyCollectionTemplateModelIterator(coll), true);
             }
-            return seq;
         }
-        
+
+        @Override
+        void enableLazilyGeneratedResult() {
+            lazilyGeneratedResultEnabled = true;
+        }
+
+        @Override
+        protected boolean isLazilyGeneratedTargetResultSupported() {
+            return true;
+        }
     }
     
     private static boolean isBuggySeqButGoodCollection(
@@ -1001,7 +1023,8 @@ class BuiltInsForSequences {
             if (elementTransformerExp instanceof LocalLambdaExpression) {
                 LocalLambdaExpression localLambdaExp = (LocalLambdaExpression) elementTransformerExp;
                 checkLocalLambdaParamCount(localLambdaExp, 1);
-                // We can't do this with other kind of expressions, as they need to be evaluated on runtime:
+                // We can't do this with other kind of expressions, like a function or method reference, as they
+                // need to be evaluated on runtime:
                 precreatedElementTransformer = new LocalLambdaElementTransformer(localLambdaExp);
             }
         }
@@ -1053,9 +1076,32 @@ class BuiltInsForSequences {
         }
 
         TemplateModel _eval(Environment env) throws TemplateException {
-            TemplateModel lho = target.eval(env);
-            TemplateModelIterator lhoIterator = getTemplateModelIterator(env, lho);
-            return calculateResult(lhoIterator, lho, evalElementTransformerExp(env), env);
+            TemplateModel targetValue = target.eval(env);
+
+            final TemplateModelIterator targetIterator;
+            final boolean targetIsSequence;
+            {
+                if (targetValue instanceof TemplateCollectionModel) {
+                    targetIterator = isLazilyGeneratedResultEnabled()
+                            ? new LazyCollectionTemplateModelIterator((TemplateCollectionModel) targetValue)
+                            : ((TemplateCollectionModel) targetValue).iterator();
+                    targetIsSequence = targetValue instanceof LazilyGeneratedCollectionModel ?
+                        ((LazilyGeneratedCollectionModel) targetValue).isSequence() : false;
+                } else if (targetValue instanceof TemplateSequenceModel) {
+                    targetIterator = new LazySequenceIterator((TemplateSequenceModel) targetValue);
+                    targetIsSequence = true;
+                } else if (targetValue instanceof TemplateModelIterator) {
+                    targetIterator = (TemplateModelIterator) targetValue;
+                    targetIsSequence = false;
+                } else {
+                    throw new NonSequenceOrCollectionException(target, targetValue, env);
+                }
+            }
+
+            return calculateResult(
+                    targetIterator, targetValue, targetIsSequence,
+                    evalElementTransformerExp(env),
+                    env);
         }
 
         private ElementTransformer evalElementTransformerExp(Environment env) throws TemplateException {
@@ -1073,31 +1119,18 @@ class BuiltInsForSequences {
             }
         }
 
-        private TemplateModelIterator getTemplateModelIterator(Environment env, TemplateModel model) throws TemplateModelException,
-                NonSequenceOrCollectionException, InvalidReferenceException {
-            if (model instanceof TemplateCollectionModel) {
-                return isLazilyGeneratedResultEnabled()
-                        ? new LazyCollectionTemplateModelIterator((TemplateCollectionModel) model)
-                        : ((TemplateCollectionModel) model).iterator();
-            } else if (model instanceof TemplateSequenceModel) {
-                return new LazySequenceIterator((TemplateSequenceModel) model);
-            } else if (model instanceof TemplateModelIterator) { // For a lazily generated LHO
-                return (TemplateModelIterator) model;
-            } else {
-                throw new NonSequenceOrCollectionException(target, model, env);
-            }
-        }
-
         /**
          * @param lhoIterator Use this to read the elements of the left hand operand
          * @param lho Maybe needed for operations specific to the built-in, like getting the size, otherwise use the
          *           {@code lhoIterator} only.
+         * @param lhoIsSequence See {@link LazilyGeneratedCollectionModel#isSequence}
          * @param elementTransformer The argument to the built-in (typically a lambda expression)
          *
          * @return {@link TemplateSequenceModel} or {@link TemplateCollectionModel} or {@link TemplateModelIterator}.
          */
         protected abstract TemplateModel calculateResult(
-                TemplateModelIterator lhoIterator, TemplateModel lho, ElementTransformer elementTransformer,
+                TemplateModelIterator lhoIterator, TemplateModel lho, boolean lhoIsSequence,
+                ElementTransformer elementTransformer,
                 Environment env) throws TemplateException;
 
         /**
@@ -1164,9 +1197,13 @@ class BuiltInsForSequences {
 
         protected TemplateModel calculateResult(
                 final TemplateModelIterator lhoIterator, final TemplateModel lho,
-                final ElementTransformer elementTransformer,
+                boolean lhoIsSequence, final ElementTransformer elementTransformer,
                 final Environment env) throws TemplateException {
             if (!isLazilyGeneratedResultEnabled()) {
+                if (!lhoIsSequence) {
+                    throw _MessageUtil.newLazilyGeneratedCollectionMustBeSequenceException(filterBI.this);
+                }
+
                 List<TemplateModel> resultList = new ArrayList<TemplateModel>();
                 while (lhoIterator.hasNext()) {
                     TemplateModel element = lhoIterator.next();
@@ -1176,7 +1213,7 @@ class BuiltInsForSequences {
                 }
                 return new TemplateModelListSequence(resultList);
             } else {
-                return new LazilyGeneratedSequenceModel(
+                return new LazilyGeneratedCollectionModelWithUnknownSize(
                         new TemplateModelIterator() {
                             boolean prefetchDone;
                             TemplateModel prefetchedElement;
@@ -1223,7 +1260,8 @@ class BuiltInsForSequences {
                                 } while (!conclusionReached);
                                 prefetchDone = true;
                             }
-                        }
+                        },
+                        lhoIsSequence
                 );
             }
         }
@@ -1250,9 +1288,13 @@ class BuiltInsForSequences {
     static class mapBI extends IntermediateStreamOperationLikeBuiltIn {
 
         protected TemplateModel calculateResult(
-                final TemplateModelIterator lhoIterator, TemplateModel lho, final ElementTransformer elementTransformer,
+                final TemplateModelIterator lhoIterator, TemplateModel lho, boolean lhoIsSequence, final ElementTransformer elementTransformer,
                 final Environment env) throws TemplateException {
             if (!isLazilyGeneratedResultEnabled()) {
+                if (!lhoIsSequence) {
+                    throw _MessageUtil.newLazilyGeneratedCollectionMustBeSequenceException(mapBI.this);
+                }
+
                 List<TemplateModel> resultList = new ArrayList<TemplateModel>();
                 while (lhoIterator.hasNext()) {
                     resultList.add(fetchAndMapNextElement(lhoIterator, elementTransformer, env));
@@ -1273,12 +1315,14 @@ class BuiltInsForSequences {
                     }
                 };
                 if (lho instanceof TemplateCollectionModelEx) { // Preferred branch, as TempCollModEx has isEmpty() too
-                    return new SameSizeCollLazilyGeneratedSequenceModel(mappedLhoIterator,
-                            (TemplateCollectionModelEx) lho);
+                    return new LazilyGeneratedCollectionModelWithSameSizeCollEx(
+                            mappedLhoIterator, (TemplateCollectionModelEx) lho, lhoIsSequence);
                 } else if (lho instanceof TemplateSequenceModel) {
-                    return new SameSizeSeqLazilyGeneratedSequenceModel(mappedLhoIterator, (TemplateSequenceModel) lho);
+                    return new LazilyGeneratedCollectionModelWithSameSizeSeq(
+                            mappedLhoIterator, (TemplateSequenceModel) lho);
                 } else {
-                    return new LazilyGeneratedSequenceModel(mappedLhoIterator);
+                    return new LazilyGeneratedCollectionModelWithUnknownSize(
+                            mappedLhoIterator, lhoIsSequence);
                 }
             }
         }
diff --git a/src/main/java/freemarker/core/DynamicKeyName.java b/src/main/java/freemarker/core/DynamicKeyName.java
index 7b57983..4a72543 100644
--- a/src/main/java/freemarker/core/DynamicKeyName.java
+++ b/src/main/java/freemarker/core/DynamicKeyName.java
@@ -114,8 +114,12 @@ final class DynamicKeyName extends Expression {
             }
             return index < size ? tsm.get(index) : null;
         }
-        if (targetModel instanceof LazilyGeneratedSequenceModel) {
-            TemplateModelIterator iter = ((LazilyGeneratedSequenceModel) targetModel).iterator();
+        if (targetModel instanceof LazilyGeneratedCollectionModel
+                && ((LazilyGeneratedCollectionModel) targetModel).isSequence()) {
+            if (index < 0) {
+                return null;
+            }
+            TemplateModelIterator iter = ((LazilyGeneratedCollectionModel) targetModel).iterator();
             for (int curIndex = 0; iter.hasNext(); curIndex++) {
                 TemplateModel next = iter.next();
                 if (index == curIndex) {
@@ -167,15 +171,16 @@ final class DynamicKeyName extends Expression {
     throws TemplateException {
         // We can have 3 kind of left hand operands ("targets"): sequence, lazily generated sequence, string
         final TemplateSequenceModel targetSeq;
-        final LazilyGeneratedSequenceModel targetLazySeq;
+        final LazilyGeneratedCollectionModel targetLazySeq;
         final String targetStr;
         if (targetModel instanceof TemplateSequenceModel) {
             targetSeq = (TemplateSequenceModel) targetModel;
             targetLazySeq = null;
             targetStr = null;
-        } else if (targetModel instanceof LazilyGeneratedSequenceModel) {
+        } else if (targetModel instanceof LazilyGeneratedCollectionModel
+                && ((LazilyGeneratedCollectionModel) targetModel).isSequence()) {
             targetSeq = null;
-            targetLazySeq = (LazilyGeneratedSequenceModel) targetModel;
+            targetLazySeq = (LazilyGeneratedCollectionModel) targetModel;
             targetStr = null;
         } else {
             targetSeq = null;
@@ -363,8 +368,8 @@ final class DynamicKeyName extends Expression {
                 }
             };
             return resultSize != UNKNOWN_RESULT_SIZE && targetSizeKnown  // targetSizeKnown => range end was validated
-                    ? new LazilyGeneratedSequenceModelWithSize(iterator, resultSize)
-                    : new LazilyGeneratedSequenceModel(iterator);
+                    ? new LazilyGeneratedCollectionModelWithAlreadyKnownSize(iterator, resultSize, true)
+                    : new LazilyGeneratedCollectionModelWithUnknownSize(iterator, true);
         } else { // !lazilyGeneratedResultEnabled
             List<TemplateModel> resultList = resultSize != UNKNOWN_RESULT_SIZE
                     ? new ArrayList<TemplateModel>(resultSize)
diff --git a/src/main/java/freemarker/core/LazilyGeneratedCollectionModel.java b/src/main/java/freemarker/core/LazilyGeneratedCollectionModel.java
new file mode 100644
index 0000000..76e42e3
--- /dev/null
+++ b/src/main/java/freemarker/core/LazilyGeneratedCollectionModel.java
@@ -0,0 +1,80 @@
+/*
+ * 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.TemplateCollectionModel;
+import freemarker.template.TemplateModelIterator;
+import freemarker.template.TemplateSequenceModel;
+
+/**
+ * Similar to {@link SingleIterationCollectionModel}, but marks the value as something that uses lazy evaluation
+ * internally, whose laziness should be transparent for the user (in FM2 at least). Values of this type shouldn't be
+ * stored without ensuring that the context needed for the generation of the lazily generated elements will be still
+ * available when the elements are read. The primary reason for the existence of this class is
+ * {@link LocalLambdaExpression}-s, which don't capture the variables they refer to, so for sequences filter/mapped
+ * by them we must ensure that all the elements are consumed before the referred variables go out of scope.
+ * <p>
+ * An operator or built-in should only ever receive a {@link LazilyGeneratedCollectionModel} if it has explicitly
+ * allowed its input expression to return such value via calling {@link Expression#enableLazilyGeneratedResult()}
+ * during parsing. With other words, an operator or built-in should only ever return
+ * {@link LazilyGeneratedCollectionModel} if it its {@link Expression#enableLazilyGeneratedResult()} method was
+ * called during the parsing.
+ * <p>
+ * Note that by accepting {@link LazilyGeneratedCollectionModel}-s the operator/built-in also undertakes taking
+ * {@link #isSequence()} into account.
+ */
+abstract class LazilyGeneratedCollectionModel extends SingleIterationCollectionModel {
+
+    private final boolean sequence;
+
+    /**
+     * @param iterator The iterator to read all the elements of this lazily generated collection.
+     * @param sequence see {@link #isSequence()}
+     */
+    protected LazilyGeneratedCollectionModel(TemplateModelIterator iterator, boolean sequence) {
+        super(iterator);
+        this.sequence = sequence;
+    }
+
+    /**
+     * If this collection is a sequence according the template author (and we only use {@link TemplateCollectionModel}
+     * internally to implement lazy generation). That means that an operator or built-in that accepts sequences must
+     * accept this {@link TemplateCollectionModel} value, instead of giving a type error. This of course only applies
+     * to operators/built-ins that accept lazy values on the first place (see
+     * {@link Expression#enableLazilyGeneratedResult}). Such operators/built-ins must implement their functionality
+     * with {@link TemplateCollectionModel} input as well, in additionally to the normal implementation with
+     * {@link TemplateSequenceModel} input. If {@link #isSequence()} returns {@code false}, and the operator/built-in
+     * doesn't support {@link TemplateCollectionModel} in general, it must fail with type error.
+     */
+    final boolean isSequence() {
+        return sequence;
+    }
+
+    /**
+     * Returns a "view" of this {@link LazilyGeneratedCollectionModel} where {@link #isSequence()} returns
+     * @code true}.
+     */
+    final LazilyGeneratedCollectionModel withIsSequenceTrue() {
+        return isSequence() ? this : withIsSequenceFromFalseToTrue();
+    }
+
+    protected abstract LazilyGeneratedCollectionModel withIsSequenceFromFalseToTrue();
+
+}
diff --git a/src/main/java/freemarker/core/LazilyGeneratedSequenceModel.java b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelEx.java
similarity index 52%
rename from src/main/java/freemarker/core/LazilyGeneratedSequenceModel.java
rename to src/main/java/freemarker/core/LazilyGeneratedCollectionModelEx.java
index 26e210a..a162768 100644
--- a/src/main/java/freemarker/core/LazilyGeneratedSequenceModel.java
+++ b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelEx.java
@@ -19,19 +19,15 @@
 
 package freemarker.core;
 
-import freemarker.ext.beans.CollectionModel;
+import freemarker.template.TemplateCollectionModelEx;
 import freemarker.template.TemplateModelIterator;
-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. 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()}.
+ * A {@link LazilyGeneratedCollectionModel} that supports {@link TemplateCollectionModelEx} methods.
  */
-class LazilyGeneratedSequenceModel extends SingleIterationCollectionModel {
-    LazilyGeneratedSequenceModel(TemplateModelIterator iterator) {
-        super(iterator);
+abstract class LazilyGeneratedCollectionModelEx extends LazilyGeneratedCollectionModel implements
+        TemplateCollectionModelEx {
+    LazilyGeneratedCollectionModelEx(TemplateModelIterator iterator, boolean sequence) {
+        super(iterator, sequence);
     }
 }
diff --git a/src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithAlreadyKnownSize.java
similarity index 71%
copy from src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java
copy to src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithAlreadyKnownSize.java
index 7faab2e..99df53f 100644
--- a/src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java
+++ b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithAlreadyKnownSize.java
@@ -19,15 +19,14 @@
 
 package freemarker.core;
 
-import freemarker.template.TemplateCollectionModelEx;
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateModelIterator;
 
-class LazilyGeneratedSequenceModelWithSize extends LazilyGeneratedSequenceModel implements TemplateCollectionModelEx {
+final class LazilyGeneratedCollectionModelWithAlreadyKnownSize extends LazilyGeneratedCollectionModelEx {
     private final int size;
 
-    LazilyGeneratedSequenceModelWithSize(TemplateModelIterator iterator, int size) {
-        super(iterator);
+    LazilyGeneratedCollectionModelWithAlreadyKnownSize(TemplateModelIterator iterator, int size, boolean sequence) {
+        super(iterator, sequence);
         this.size = size;
     }
 
@@ -38,4 +37,9 @@ class LazilyGeneratedSequenceModelWithSize extends LazilyGeneratedSequenceModel
     public boolean isEmpty() {
         return size == 0;
     }
+
+    @Override
+    protected LazilyGeneratedCollectionModel withIsSequenceFromFalseToTrue() {
+        return new LazilyGeneratedCollectionModelWithAlreadyKnownSize(getIterator(), size, true);
+    }
 }
diff --git a/src/main/java/freemarker/core/SameSizeLazilyGeneratedSequenceModel.java b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithSameSizeCollEx.java
similarity index 72%
rename from src/main/java/freemarker/core/SameSizeLazilyGeneratedSequenceModel.java
rename to src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithSameSizeCollEx.java
index a0f4225..6917273 100644
--- a/src/main/java/freemarker/core/SameSizeLazilyGeneratedSequenceModel.java
+++ b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithSameSizeCollEx.java
@@ -25,16 +25,15 @@ import freemarker.template.TemplateModelIterator;
 import freemarker.template.utility.NullArgumentException;
 
 /**
- * Used instead of {@link LazilyGeneratedSequenceModel} for operations that don't change the element count of the
+ * Used instead of {@link LazilyGeneratedCollectionModel} for operations that don't change the element count of the
  * source, if the source can also give back an element count.
  */
-class SameSizeCollLazilyGeneratedSequenceModel extends LazilyGeneratedSequenceModel
-        implements TemplateCollectionModelEx {
+class LazilyGeneratedCollectionModelWithSameSizeCollEx extends LazilyGeneratedCollectionModelEx {
     private final TemplateCollectionModelEx sizeSourceCollEx;
 
-    public SameSizeCollLazilyGeneratedSequenceModel(
-            TemplateModelIterator iterator, TemplateCollectionModelEx sizeSourceCollEx) {
-        super(iterator);
+    public LazilyGeneratedCollectionModelWithSameSizeCollEx(
+            TemplateModelIterator iterator, TemplateCollectionModelEx sizeSourceCollEx, boolean sequenceSourced) {
+        super(iterator, sequenceSourced);
         NullArgumentException.check(sizeSourceCollEx);
         this.sizeSourceCollEx = sizeSourceCollEx;
     }
@@ -46,4 +45,9 @@ class SameSizeCollLazilyGeneratedSequenceModel extends LazilyGeneratedSequenceMo
     public boolean isEmpty() throws TemplateModelException {
         return sizeSourceCollEx.isEmpty();
     }
+
+    @Override
+    protected LazilyGeneratedCollectionModelWithSameSizeCollEx withIsSequenceFromFalseToTrue() {
+        return new LazilyGeneratedCollectionModelWithSameSizeCollEx(getIterator(), sizeSourceCollEx, true);
+    }
 }
diff --git a/src/main/java/freemarker/core/SameSizeSeqLazilyGeneratedSequenceModel.java b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithSameSizeSeq.java
similarity index 77%
rename from src/main/java/freemarker/core/SameSizeSeqLazilyGeneratedSequenceModel.java
rename to src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithSameSizeSeq.java
index d6d7b5f..15e7c4a 100644
--- a/src/main/java/freemarker/core/SameSizeSeqLazilyGeneratedSequenceModel.java
+++ b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithSameSizeSeq.java
@@ -19,25 +19,21 @@
 
 package freemarker.core;
 
-import freemarker.template.TemplateCollectionModelEx;
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateModelIterator;
 import freemarker.template.TemplateSequenceModel;
 import freemarker.template.utility.NullArgumentException;
 
 /**
- * Used instead of {@link LazilyGeneratedSequenceModel} for operations that don't change the element count of the
+ * Used instead of {@link LazilyGeneratedCollectionModel} for operations that don't change the element count of the
  * source, if the source can also give back an element count.
- *
- * @since 2.3.29
  */
-class SameSizeSeqLazilyGeneratedSequenceModel extends LazilyGeneratedSequenceModel
-        implements TemplateCollectionModelEx {
+class LazilyGeneratedCollectionModelWithSameSizeSeq extends LazilyGeneratedCollectionModelEx {
     private final TemplateSequenceModel sizeSourceSeq;
 
-    public SameSizeSeqLazilyGeneratedSequenceModel(
+    public LazilyGeneratedCollectionModelWithSameSizeSeq(
             TemplateModelIterator iterator, TemplateSequenceModel sizeSourceSeq) {
-        super(iterator);
+        super(iterator, true);
         NullArgumentException.check(sizeSourceSeq);
         this.sizeSourceSeq = sizeSourceSeq;
     }
@@ -49,4 +45,9 @@ class SameSizeSeqLazilyGeneratedSequenceModel extends LazilyGeneratedSequenceMod
     public boolean isEmpty() throws TemplateModelException {
         return sizeSourceSeq.size() == 0;
     }
+
+    @Override
+    protected LazilyGeneratedCollectionModelWithSameSizeSeq withIsSequenceFromFalseToTrue() {
+        return this; // Won't be actually called...
+    }
 }
diff --git a/src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithUnknownSize.java
similarity index 63%
rename from src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java
rename to src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithUnknownSize.java
index 7faab2e..53ec1e8 100644
--- a/src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java
+++ b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithUnknownSize.java
@@ -19,23 +19,15 @@
 
 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 final class LazilyGeneratedCollectionModelWithUnknownSize extends LazilyGeneratedCollectionModel {
+    public LazilyGeneratedCollectionModelWithUnknownSize(TemplateModelIterator iterator, boolean sequence) {
+        super(iterator, sequence);
     }
 
-    public boolean isEmpty() {
-        return size == 0;
+    @Override
+    protected LazilyGeneratedCollectionModelWithUnknownSize withIsSequenceFromFalseToTrue() {
+        return new LazilyGeneratedCollectionModelWithUnknownSize(getIterator(), true);
     }
 }
diff --git a/src/main/java/freemarker/core/SingleIterationCollectionModel.java b/src/main/java/freemarker/core/SingleIterationCollectionModel.java
index 56efe93..f8e8471 100644
--- a/src/main/java/freemarker/core/SingleIterationCollectionModel.java
+++ b/src/main/java/freemarker/core/SingleIterationCollectionModel.java
@@ -48,4 +48,8 @@ class SingleIterationCollectionModel implements TemplateCollectionModel {
         iterator = null;
         return result;
     }
+
+    protected TemplateModelIterator getIterator() {
+        return iterator;
+    }
 }
diff --git a/src/main/java/freemarker/core/_CoreAPI.java b/src/main/java/freemarker/core/_CoreAPI.java
index 7b0596b..55a2d3d 100644
--- a/src/main/java/freemarker/core/_CoreAPI.java
+++ b/src/main/java/freemarker/core/_CoreAPI.java
@@ -27,6 +27,7 @@ import java.util.TreeSet;
 
 import freemarker.template.Configuration;
 import freemarker.template.Template;
+import freemarker.template.TemplateCollectionModel;
 import freemarker.template.TemplateDirectiveBody;
 import freemarker.template.TemplateException;
 import freemarker.template.TemplateModel;
@@ -219,7 +220,7 @@ public class _CoreAPI {
         parser.setPreventStrippings(preventStrippings);
     }
 
-    public static boolean isLazilyGeneratedSequenceModel(Class cl) {
-        return LazilyGeneratedSequenceModel.class.isAssignableFrom(cl);
+    public static boolean isLazilyGeneratedSequenceModel(TemplateCollectionModel model) {
+        return model instanceof LazilyGeneratedCollectionModel && ((LazilyGeneratedCollectionModel) model).isSequence();
     }
 }
diff --git a/src/main/java/freemarker/core/_MessageUtil.java b/src/main/java/freemarker/core/_MessageUtil.java
index 6492607..4d2df6a 100644
--- a/src/main/java/freemarker/core/_MessageUtil.java
+++ b/src/main/java/freemarker/core/_MessageUtil.java
@@ -341,22 +341,31 @@ public class _MessageUtil {
                         new _DelayedShortClassName(TemplateHashModelEx2.class),
                         ", which leads to this restriction."));
     }
-    
+
+    public static TemplateException newLazilyGeneratedCollectionMustBeSequenceException(Expression blamed) {
+        return new _MiscTemplateException(blamed,
+                "The result is a listable value with lazy transformation(s) applied on it, but it's not " +
+                "an FTL sequence (it's not a List-like value, but an Iterator-like value). The place doesn't " +
+                "support such values due to technical limitations. So either pass it to a construct that supports " +
+                "such values (like ", "<#list transformedListable as x>", "), or, if you know that you don't have " +
+                "too many elements, use transformedListable?sequence to allow it to be treated as an FTL sequence.");
+    }
+
     /**
      * @return "a" or "an" or "a(n)" (or "" for empty string) for an FTL type name
      */
     static public String getAOrAn(String s) {
         if (s == null) return null;
         if (s.length() == 0) return "";
-        
+
         char fc = Character.toLowerCase(s.charAt(0));
         if (fc == 'a' || fc == 'e' || fc == 'i') {
             return "an";
-        } else if (fc == 'h') { 
+        } else if (fc == 'h') {
             String ls = s.toLowerCase();
-            if (ls.startsWith("has") || ls.startsWith("hi")) { 
+            if (ls.startsWith("has") || ls.startsWith("hi")) {
                 return "a";
-            } else if (ls.startsWith("ht")) { 
+            } else if (ls.startsWith("ht")) {
                 return "an";
             } else {
                 return "a(n)";
@@ -364,7 +373,7 @@ public class _MessageUtil {
         } else if (fc == 'u' || fc == 'o') {
             return "a(n)";
         } else {
-            char sc = (s.length() > 1) ? s.charAt(1) : '\0'; 
+            char sc = (s.length() > 1) ? s.charAt(1) : '\0';
             if (fc == 'x' && !(sc == 'a' || sc == 'e' || sc == 'i' || sc == 'a' || sc == 'o' || sc == 'u')) {
                 return "an";
             } else {
@@ -372,5 +381,4 @@ public class _MessageUtil {
             }
         }
     }
-    
 }
diff --git a/src/main/java/freemarker/template/utility/ClassUtil.java b/src/main/java/freemarker/template/utility/ClassUtil.java
index 45508df..ad19750 100644
--- a/src/main/java/freemarker/template/utility/ClassUtil.java
+++ b/src/main/java/freemarker/template/utility/ClassUtil.java
@@ -188,6 +188,9 @@ public class ClassUtil {
             }
         } else if (tm instanceof SimpleMethodModel || tm instanceof OverloadedMethodsModel) {
             return TemplateMethodModelEx.class;
+        } else if (tm instanceof TemplateCollectionModel
+                && _CoreAPI.isLazilyGeneratedSequenceModel((TemplateCollectionModel) tm)) {
+            return TemplateSequenceModel.class;
         } else {
             return null;
         }
@@ -208,7 +211,7 @@ public class ClassUtil {
             appendTypeName(sb, typeNamesAppended, "transform");
         }
         
-        if (TemplateSequenceModel.class.isAssignableFrom(cl) || _CoreAPI.isLazilyGeneratedSequenceModel(cl)) {
+        if (TemplateSequenceModel.class.isAssignableFrom(cl)) {
             appendTypeName(sb, typeNamesAppended, "sequence");
         } else if (TemplateCollectionModel.class.isAssignableFrom(cl)) {
             appendTypeName(sb, typeNamesAppended,
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 3285d9b..931f768 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -27933,6 +27933,32 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
             </listitem>
 
             <listitem>
+              <para><link
+              linkend="ref_builtin_sequence"><literal>?sequence</literal></link>
+              now collaborates with
+              <literal><replaceable>seq</replaceable>?size</literal>,
+              <literal><replaceable>seq</replaceable>[<replaceable>index</replaceable>]</literal>,
+              <literal><replaceable>seq</replaceable>[<replaceable>range</replaceable>]</literal>,
+              and with some built-ins (<literal>filter</literal>,
+              <literal>map</literal>, <literal>join</literal>, etc.) to spare
+              collecting all the elements into the memory when possible. For
+              example <literal>anIterator?sequence[1]</literal> now will just
+              fetch the first 2 items, instead of building a sequence that
+              contains all the elements, and then getting the 2nd element from
+              that. Or, if you write
+              <literal>anIterator?sequence?size</literal>, it will just skip
+              through all elements to count them, but won't store them in
+              memory. These optimizations only work within the same chain of
+              built-in calls, so for example in <literal>&lt;#assign seq =
+              anIterator?sequence&gt;${seq[1]}</literal> will still collect
+              all the elements into the memory, as
+              <literal>anIterator?sequence</literal> and
+              <literal>seq[1]</literal> are separated. [TODO: document this at
+              <link linkend="ref_builtin_sequence">ref_builtin_sequence</link>
+              too]</para>
+            </listitem>
+
+            <listitem>
               <para>Extended big decimal format parameter
               <quote>multiplier</quote> was incorrectly written as
               <quote>multipier</quote>. Now both words are recognized.</para>
diff --git a/src/test/java/freemarker/core/FilterBiTest.java b/src/test/java/freemarker/core/FilterBiTest.java
index 14e37ac..ee16dff 100644
--- a/src/test/java/freemarker/core/FilterBiTest.java
+++ b/src/test/java/freemarker/core/FilterBiTest.java
@@ -24,7 +24,10 @@ import java.util.List;
 import org.junit.Test;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
 import freemarker.template.TemplateException;
 import freemarker.test.TemplateTest;
 
@@ -40,6 +43,17 @@ public class FilterBiTest extends TemplateTest {
         }
     }
 
+    @Override
+    protected Configuration createConfiguration() throws Exception {
+        Configuration cfg = super.createConfiguration();
+
+        DefaultObjectWrapper objectWrapper = new DefaultObjectWrapper(Configuration.VERSION_2_3_28);
+        objectWrapper.setForceLegacyNonListCollections(false);
+        cfg.setObjectWrapper(objectWrapper);
+
+        return cfg;
+    }
+
     private static final List<TestParam> TEST_PARAMS = ImmutableList.of(
             new TestParam(ImmutableList.of("a", "aX", "bX", "b", "cX", "c"), "a, b, c"),
             new TestParam(ImmutableList.of("a", "b", "c"), "a, b, c"),
@@ -130,6 +144,17 @@ public class FilterBiTest extends TemplateTest {
                 "lambda", "1 parameter", "declared 2");
     }
 
+    @Test
+    public void testNonSequenceInput() throws Exception {
+        addToDataModel("coll", ImmutableSet.of("a", "b", "c"));
+        assertErrorContains("${coll?filter(it -> it != 'a')[0]}", "sequence", "evaluated to a collection");
+        assertErrorContains("[#ftl][#assign t = coll?filter(it -> it != 'a')]",
+                "lazy transformation", "?sequence", "[#list");
+        assertOutput("${coll?sequence?filter(it -> it != 'a')[0]}", "b");
+        assertOutput("${coll?filter(it -> it != 'a')?sequence[0]}", "b");
+        assertOutput("<#list coll?filter(it -> it != 'a') as it>${it}</#list>", "bc");
+    }
+
     public static class FilterObject {
         public boolean noX(String s) {
             return !s.contains("X");
diff --git a/src/test/java/freemarker/core/LazilyGeneratedSeqTest.java b/src/test/java/freemarker/core/LazilyGeneratedCollectionTest.java
similarity index 80%
rename from src/test/java/freemarker/core/LazilyGeneratedSeqTest.java
rename to src/test/java/freemarker/core/LazilyGeneratedCollectionTest.java
index 0886f05..fbbf226 100644
--- a/src/test/java/freemarker/core/LazilyGeneratedSeqTest.java
+++ b/src/test/java/freemarker/core/LazilyGeneratedCollectionTest.java
@@ -28,6 +28,7 @@ import java.util.List;
 import org.junit.Test;
 
 import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
 import freemarker.template.SimpleNumber;
 import freemarker.template.TemplateCollectionModel;
 import freemarker.template.TemplateCollectionModelEx;
@@ -38,33 +39,67 @@ import freemarker.template.TemplateSequenceModel;
 import freemarker.test.TemplateTest;
 
 /**
- * Tests operators and built-ins that are support getting {@link LazilyGeneratedSequenceModel} as their operands.
+ * Tests operators and built-ins that are support getting and/or returning {@link LazilyGeneratedCollectionModel}.
+ * @see MapBiTest
+ * @see FilterBiTest
  */
-public class LazilyGeneratedSeqTest extends TemplateTest {
+public class LazilyGeneratedCollectionTest extends TemplateTest {
+
+    @Override
+    protected Configuration createConfiguration() throws Exception {
+        Configuration cfg = super.createConfiguration();
+        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));
+
+        DefaultObjectWrapper objectWrapper = new DefaultObjectWrapper(Configuration.VERSION_2_3_28);
+        objectWrapper.setForceLegacyNonListCollections(false);
+        cfg.setObjectWrapper(objectWrapper);
+
+        return cfg;
+    }
 
     @Test
     public void dynamicIndexTest() throws Exception {
-        assertErrorContains("${coll?map(it -> it)['x']}",
+        assertErrorContains("${coll?sequence?map(it -> it)['x']}",
                 "hash", "evaluated to a sequence");
 
-        assertOutput("${coll?map(it -> it)[0]}",
+        assertOutput("${coll?sequence?map(it -> it)[0]}",
                 "[iterator][hasNext][next]1");
-        assertOutput("${coll?map(it -> it)[1]}",
+        assertOutput("${coll?sequence?map(it -> it)[1]}",
                 "[iterator][hasNext][next][hasNext][next]2");
-        assertOutput("${coll?map(it -> it)[2]}",
+        assertOutput("${coll?map(it -> it)?sequence[1]}",
+                "[iterator][hasNext][next][hasNext][next]2");
+        assertOutput("${coll?sequence?map(it -> it)[2]}",
                 "[iterator][hasNext][next][hasNext][next][hasNext][next]3");
-        assertOutput("${coll?map(it -> it)[3]!'missing'}",
+        assertOutput("${coll?sequence?map(it -> it)[3]!'missing'}",
                 "[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]missing");
-        assertOutput("${coll?filter(it -> it % 2 == 0)[0]}",
+        assertOutput("${coll?sequence?filter(it -> it % 2 == 0)[0]}",
                 "[iterator][hasNext][next][hasNext][next]2");
-        assertOutput("${coll?filter(it -> it > 3)[0]!'missing'}",
+        assertOutput("${coll?sequence?filter(it -> it > 3)[0]!'missing'}",
                 "[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]missing");
 
-        assertOutput("${collLong?map(it -> it)[1 .. 2]?join(', ')}",
+        assertOutput("${collLong?sequence?map(it -> it)[1 .. 2]?join(', ')}",
                 "[iterator][hasNext][next][hasNext][next][hasNext][next]2, 3");
     }
 
     @Test
+    public void dynamicIndexNonSequenceInput() throws Exception {
+        assertErrorContains("${coll[1]}", "sequence", "evaluated to a collection");
+        assertOutput("${coll?sequence[1]}", "[iterator][hasNext][next][hasNext][next]2");
+
+        assertErrorContains("<#assign t = coll[1..2]>", "sequence", "evaluated to a collection");
+        assertOutput("<#assign t = coll?sequence[1..2]>${t?join('')}",
+                "[iterator][hasNext][next][hasNext][next][hasNext][next]23");
+        assertOutput("<#list coll?sequence[1..2] as it>${it}</#list>",
+                "[iterator][hasNext][next][hasNext][next]2[hasNext][next]3");
+    }
+
+    @Test
     public void sizeBasicsTest() throws Exception {
         assertOutput("${seq?size}",
                 "[size]3");
@@ -75,12 +110,16 @@ public class LazilyGeneratedSeqTest extends TemplateTest {
 
         assertOutput("${seq?map(x -> x * 10)?size}",
                 "[size]3");
-        assertOutput("${collEx?map(x -> x * 10)?size}",
+        assertOutput("${collEx?sequence?map(x -> x * 10)?size}",
+                "[size]3");
+        assertOutput("${collEx?map(x -> x * 10)?sequence?size}",
                 "[size]3");
 
         assertOutput("${seq?filter(x -> x != 1)?size}",
                 "[size][get 0][get 1][get 2]2");
-        assertOutput("${collEx?filter(x -> x != 1)?size}",
+        assertOutput("${collEx?sequence?filter(x -> x != 1)?size}",
+                "[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]2");
+        assertOutput("${collEx?filter(x -> x != 1)?sequence?size}",
                 "[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]2");
     }
 
@@ -119,22 +158,30 @@ public class LazilyGeneratedSeqTest extends TemplateTest {
                 "[size]false");
 
         // Now the lazy generation things:
-        assertOutput("${collLong?filter(x -> true)?size}",
+        assertOutput("${collLong?sequence?filter(x -> true)?size}",
                 "[iterator]" +
                         "[hasNext][next][hasNext][next][hasNext][next]" +
                         "[hasNext][next][hasNext][next][hasNext][next][hasNext]6");
         // Note: "[next]" is added by ?filter, as it has to know if the element matches the predicate.
-        assertOutput("${collLong?filter(x -> true)?size != 0}",
+        assertOutput("${collLong?sequence?filter(x -> true)?size != 0}",
                 "[iterator][hasNext][next]true");
-        assertOutput("${collLong?filter(x -> true)?size != 1}",
+        assertOutput("${collLong?sequence?filter(x -> true)?size != 1}",
                 "[iterator][hasNext][next][hasNext][next]true");
-        assertOutput("${collLong?filter(x -> true)?size == 1}",
+        assertOutput("${collLong?sequence?filter(x -> true)?size == 1}",
+                "[iterator][hasNext][next][hasNext][next]false");
+        assertOutput("${collLong?filter(x -> true)?sequence?size == 1}",
                 "[iterator][hasNext][next][hasNext][next]false");
-        assertOutput("${collLong?filter(x -> true)?size < 3}",
+        assertOutput("${collLong?sequence?filter(x -> true)?size < 3}",
                 "[iterator][hasNext][next][hasNext][next][hasNext][next]false");
     }
 
     @Test
+    public void sizeNonSequenceInput() throws Exception {
+        assertErrorContains("${coll?size}", "sequence", "evaluated to a collection");
+        assertOutput("${coll?sequence?size}", "[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]3");
+    }
+
+    @Test
     public void firstTest() throws Exception {
         assertOutput("${coll?first}",
                 "[iterator][hasNext][next]1");
@@ -218,19 +265,6 @@ public class LazilyGeneratedSeqTest extends TemplateTest {
         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 {
-        Configuration cfg = super.createConfiguration();
-        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));
-        return cfg;
-    }
-
     public static abstract class ListContainingTemplateModel {
         protected final List<Number> elements;
 
diff --git a/src/test/java/freemarker/core/MapBiTest.java b/src/test/java/freemarker/core/MapBiTest.java
index 49934be..3e70c0f 100644
--- a/src/test/java/freemarker/core/MapBiTest.java
+++ b/src/test/java/freemarker/core/MapBiTest.java
@@ -25,8 +25,10 @@ import java.util.List;
 import org.junit.Test;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 
 import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
 import freemarker.template.TemplateException;
 import freemarker.test.TemplateTest;
 
@@ -51,8 +53,14 @@ public class MapBiTest extends TemplateTest {
     @Override
     protected Configuration createConfiguration() throws Exception {
         Configuration cfg = super.createConfiguration();
+
         cfg.setNumberFormat("0.####");
         cfg.setBooleanFormat("c");
+
+        DefaultObjectWrapper objectWrapper = new DefaultObjectWrapper(Configuration.VERSION_2_3_28);
+        objectWrapper.setForceLegacyNonListCollections(false);
+        cfg.setObjectWrapper(objectWrapper);
+
         return cfg;
     }
 
@@ -181,6 +189,17 @@ public class MapBiTest extends TemplateTest {
                 "lambda", "1 parameter", "declared 2");
     }
 
+    @Test
+    public void testNonSequenceInput() throws Exception {
+        addToDataModel("coll", ImmutableSet.of("a", "b", "c"));
+        assertErrorContains("${coll?map(it -> it?upperCase)[0]}", "sequence", "evaluated to an extended_collection");
+        assertErrorContains("[#ftl][#assign t = coll?map(it -> it?upperCase)]",
+                "lazy transformation", "?sequence", "[#list");
+        assertOutput("${coll?sequence?map(it -> it?upperCase)[0]}", "A");
+        assertOutput("${coll?map(it -> it?upperCase)?sequence[0]}", "A");
+        assertOutput("<#list coll?map(it -> it?upperCase) as it>${it}</#list>", "ABC");
+    }
+
     public static class MapperObject {
         public String toUpper(String s) {
             return s.toUpperCase();