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 2017/08/07 22:32:05 UTC

[03/21] incubator-freemarker git commit: FREEMARKER-63: Very early state. Added support for nested content and loop variables. Added StringToIndexMap, which is used for mapping names to their index in the value arrays (a concept that we use at a few pla

FREEMARKER-63:  Very early state. Added support for nested content and loop variables. Added StringToIndexMap, which is used for mapping names to their index in the value arrays (a concept that we use at a few places now).


Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/46c75010
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/46c75010
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/46c75010

Branch: refs/heads/3
Commit: 46c750109ffe9e4c7e52e4c3df94b78c3445f780
Parents: c28a78b
Author: ddekany <dd...@apache.org>
Authored: Wed Jul 26 23:48:11 2017 +0200
Committer: ddekany <dd...@apache.org>
Committed: Wed Jul 26 23:48:11 2017 +0200

----------------------------------------------------------------------
 .../core/TemplateCallableModelTest.java         |  20 +-
 .../core/userpkg/AllFeaturesDirective.java      |  36 +-
 .../core/userpkg/TwoNamedParamsDirective.java   |  16 +-
 .../core/util/StringToIndexMapTest.java         | 164 +++++++
 .../freemarker/core/ASTDirDynamicCall.java      | 439 +++++++++++++++++++
 .../core/ASTDirDynamicDirectiveCall.java        | 422 ------------------
 .../org/apache/freemarker/core/Environment.java |  38 +-
 .../apache/freemarker/core/model/CallPlace.java |   6 +-
 .../core/model/TemplateCallableModel.java       |   3 +
 .../freemarker/core/util/StringToIndexMap.java  | 316 +++++++++++++
 .../apache/freemarker/core/util/_ArrayList.java |  58 +++
 freemarker-core/src/main/javacc/FTL.jj          |  32 +-
 12 files changed, 1079 insertions(+), 471 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/46c75010/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateCallableModelTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateCallableModelTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateCallableModelTest.java
index 67b73d0..536727a 100644
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateCallableModelTest.java
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateCallableModelTest.java
@@ -38,7 +38,7 @@ public class TemplateCallableModelTest extends TemplateTest {
     }
 
     @Test
-    public void testBasicCall() throws IOException, TemplateException {
+    public void testArguments() throws IOException, TemplateException {
         assertOutput("<~p />",
                 "#p(p1=null, p2=null)");
         assertOutput("<~p 1 />",
@@ -80,11 +80,25 @@ public class TemplateCallableModelTest extends TemplateTest {
                 "#a(p1=null, p2=null, pOthers=[], n1=11, n2=22, nOthers={}; 3)");
         assertOutput("<~a 1, 2 n1=11 n2=22; a, b, c />",
                 "#a(p1=1, p2=2, pOthers=[], n1=11, n2=22, nOthers={}; 3)");
+    }
 
+    @Test
+    public void testNestedContent() throws IOException, TemplateException {
+        assertOutput("<~a />",
+                "#a(p1=null, p2=null, pOthers=[], n1=null, n2=null, nOthers={})");
         assertOutput("<~a></~a>",
                 "#a(p1=null, p2=null, pOthers=[], n1=null, n2=null, nOthers={})");
+
         assertOutput("<~a>x</~a>",
-                "#a(p1=null, p2=null, pOthers=[], n1=null, n2=null, nOthers={}) {...}");
+                "#a(p1=null, p2=null, pOthers=[], n1=null, n2=null, nOthers={}) {}");
+        assertOutput("<~a 1>x</~a>",
+                "#a(p1=1, p2=null, pOthers=[], n1=null, n2=null, nOthers={}) {x}");
+        assertOutput("<~a 3>x</~a>",
+                "#a(p1=3, p2=null, pOthers=[], n1=null, n2=null, nOthers={}) {xxx}");
+        assertOutput("<~a 3; i, j, k>[${i}${j}${k}]</~a>",
+                "#a(p1=3, p2=null, pOthers=[], n1=null, n2=null, nOthers={}; 3) {[123][246][369]}");
+        assertOutput("<#assign i = '-'>${i} <~a 3; i>${i}</~a> ${i}",
+                "- #a(p1=3, p2=null, pOthers=[], n1=null, n2=null, nOthers={}; 1) {123} -");
     }
 
     @Test
@@ -109,6 +123,7 @@ public class TemplateCallableModelTest extends TemplateTest {
     }
 
     @Test
+    @SuppressWarnings("ThrowableNotThrown")
     public void testParsingErrors() throws IOException, TemplateException {
         assertErrorContains("<~a, n1=1 />", "Remove comma", "between", "by position");
         assertErrorContains("<~a n1=1, n2=1 />", "Remove comma", "between", "by position");
@@ -120,6 +135,7 @@ public class TemplateCallableModelTest extends TemplateTest {
     }
 
     @Test
+    @SuppressWarnings("ThrowableNotThrown")
     public void testRuntimeErrors() throws IOException, TemplateException {
         assertErrorContains("<~p 9, 9, 9 />", "can only have 2", "3", "by position");
         assertErrorContains("<~n 9 />", "can't have arguments passed by position");

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/46c75010/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AllFeaturesDirective.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AllFeaturesDirective.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AllFeaturesDirective.java
index b7baf56..91e6d2a 100644
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AllFeaturesDirective.java
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AllFeaturesDirective.java
@@ -24,7 +24,6 @@ import static org.apache.freemarker.core.TemplateCallableModelUtils.*;
 import java.io.IOException;
 import java.io.Writer;
 import java.util.Collection;
-import java.util.Map;
 
 import org.apache.freemarker.core.Environment;
 import org.apache.freemarker.core.TemplateException;
@@ -33,8 +32,8 @@ import org.apache.freemarker.core.model.TemplateHashModelEx2;
 import org.apache.freemarker.core.model.TemplateModel;
 import org.apache.freemarker.core.model.TemplateNumberModel;
 import org.apache.freemarker.core.model.TemplateSequenceModel;
-
-import com.google.common.collect.ImmutableMap;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.util.StringToIndexMap;
 
 public class AllFeaturesDirective extends TestTemplateDirectiveModel {
 
@@ -64,10 +63,9 @@ public class AllFeaturesDirective extends TestTemplateDirectiveModel {
         this.n2AllowNull = n2AllowNull;
     }
 
-    private static final Map<String, Integer> PARAM_NAME_TO_IDX = new ImmutableMap.Builder<String, Integer>()
-            .put(N1_ARG_NAME, N1_ARG_IDX)
-            .put(N2_ARG_NAME, N2_ARG_IDX)
-            .build();
+    private static final StringToIndexMap PARAM_NAME_TO_IDX = StringToIndexMap.of(
+            N1_ARG_NAME, N1_ARG_IDX,
+            N2_ARG_NAME, N2_ARG_IDX);
 
     @Override
     public void execute(TemplateModel[] args, Writer out, Environment env, CallPlace callPlace)
@@ -91,12 +89,25 @@ public class AllFeaturesDirective extends TestTemplateDirectiveModel {
         printParam(N1_ARG_NAME, n1, out);
         printParam(N2_ARG_NAME, n2, out);
         printParam("nOthers", nOthers, out);
-        if (callPlace.getLoopVariableCount() != 0) {
-            out.write("; " + callPlace.getLoopVariableCount());
+        int loopVariableCount = callPlace.getLoopVariableCount();
+        if (loopVariableCount != 0) {
+            out.write("; " + loopVariableCount);
         }
         out.write(")");
+
         if (callPlace.hasNestedContent()) {
-            out.write(" {...}");
+            out.write(" {");
+            if (p1 != null) {
+                int intP1 = p1.getAsNumber().intValue();
+                for (int i = 0; i < intP1; i++) {
+                    TemplateModel[] loopVariableValues = new TemplateModel[loopVariableCount];
+                    for (int loopVarIdx = 0; loopVarIdx < loopVariableCount; loopVarIdx++) {
+                        loopVariableValues[loopVarIdx] = new SimpleNumber((i + 1) * (loopVarIdx + 1));
+                    }
+                    callPlace.executeNestedContent(loopVariableValues, env);
+                }
+            }
+            out.write("}");
         }
     }
 
@@ -112,8 +123,7 @@ public class AllFeaturesDirective extends TestTemplateDirectiveModel {
 
     @Override
     public int getNamedArgumentIndex(String name) {
-        Integer idx = PARAM_NAME_TO_IDX.get(name);
-        return idx != null ? idx : -1;
+        return PARAM_NAME_TO_IDX.get(name);
     }
 
     @Override
@@ -123,7 +133,7 @@ public class AllFeaturesDirective extends TestTemplateDirectiveModel {
 
     @Override
     public Collection<String> getPredefinedNamedArgumentNames() {
-        return PARAM_NAME_TO_IDX.keySet();
+        return PARAM_NAME_TO_IDX.getKeys();
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/46c75010/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoNamedParamsDirective.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoNamedParamsDirective.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoNamedParamsDirective.java
index 4d2d635..8c58853 100644
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoNamedParamsDirective.java
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoNamedParamsDirective.java
@@ -22,14 +22,12 @@ package org.apache.freemarker.core.userpkg;
 import java.io.IOException;
 import java.io.Writer;
 import java.util.Collection;
-import java.util.Map;
 
 import org.apache.freemarker.core.Environment;
 import org.apache.freemarker.core.TemplateException;
 import org.apache.freemarker.core.model.CallPlace;
 import org.apache.freemarker.core.model.TemplateModel;
-
-import com.google.common.collect.ImmutableMap;
+import org.apache.freemarker.core.util.StringToIndexMap;
 
 public class TwoNamedParamsDirective extends TestTemplateDirectiveModel {
 
@@ -38,10 +36,9 @@ public class TwoNamedParamsDirective extends TestTemplateDirectiveModel {
     private static final int N1_ARG_IDX = 0;
     private static final int N2_ARG_IDX = 1;
 
-    private static final Map<String, Integer> PARAM_NAME_TO_IDX = new ImmutableMap.Builder<String, Integer>()
-            .put(N1_ARG_NAME, N1_ARG_IDX)
-            .put(N2_ARG_NAME, N2_ARG_IDX)
-            .build();
+    private static final StringToIndexMap PARAM_NAME_TO_IDX = StringToIndexMap.of(
+            N1_ARG_NAME, N1_ARG_IDX,
+            N2_ARG_NAME, N2_ARG_IDX);
 
     @Override
     public void execute(TemplateModel[] args, Writer out, Environment env, CallPlace callPlace)
@@ -64,8 +61,7 @@ public class TwoNamedParamsDirective extends TestTemplateDirectiveModel {
 
     @Override
     public int getNamedArgumentIndex(String name) {
-        Integer idx = PARAM_NAME_TO_IDX.get(name);
-        return idx != null ? idx : -1;
+        return PARAM_NAME_TO_IDX.get(name);
     }
 
     @Override
@@ -80,6 +76,6 @@ public class TwoNamedParamsDirective extends TestTemplateDirectiveModel {
 
     @Override
     public Collection<String> getPredefinedNamedArgumentNames() {
-        return PARAM_NAME_TO_IDX.keySet();
+        return PARAM_NAME_TO_IDX.getKeys();
     }
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/46c75010/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/StringToIndexMapTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/StringToIndexMapTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/StringToIndexMapTest.java
new file mode 100644
index 0000000..769325f
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/util/StringToIndexMapTest.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.util;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import org.apache.freemarker.core.util.StringToIndexMap.Entry;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class StringToIndexMapTest {
+
+    @Test
+    public void testEmpty() {
+        StringToIndexMap m = StringToIndexMap.EMPTY;
+        assertEquals(0, m.size());
+        assertEquals(-1, m.get("a"));
+        assertTrue(m.getKeys().isEmpty());
+
+        assertSame(m, StringToIndexMap.of());
+        assertSame(m, StringToIndexMap.of(new Entry[0]));
+    }
+
+    @Test
+    public void testSize1() {
+        StringToIndexMap m = StringToIndexMap.of("i", 0);
+        assertEquals(1, m.size());
+        assertEquals(-1, m.get("a"));
+        assertEquals(0, m.get("i"));
+        assertEquals(ImmutableList.of("i"), m.getKeys());
+    }
+
+    @Test
+    public void testSize2() {
+        StringToIndexMap m = StringToIndexMap.of("i", 0, "j", 1);
+        assertEquals(2, m.size());
+        assertEquals(-1, m.get("a"));
+        assertEquals(0, m.get("i"));
+        assertEquals(1, m.get("j"));
+        assertEquals(ImmutableList.of("i", "j"), m.getKeys());
+    }
+
+    @Test
+    public void testSize3() {
+        StringToIndexMap m = StringToIndexMap.of("i", 0, "j", 1, "k", 2);
+        assertEquals(3, m.size());
+        assertEquals(-1, m.get("a"));
+        assertEquals(0, m.get("i"));
+        assertEquals(1, m.get("j"));
+        assertEquals(2, m.get("k"));
+        assertEquals(ImmutableList.of("i", "j", "k"), m.getKeys());
+    }
+
+    @Test
+    public void testSize4() {
+        StringToIndexMap m = StringToIndexMap.of("i", 0, "j", 1, "k", 2, "l", 3);
+        assertEquals(4, m.size());
+        assertEquals(-1, m.get("a"));
+        assertEquals(0, m.get("i"));
+        assertEquals(1, m.get("j"));
+        assertEquals(2, m.get("k"));
+        assertEquals(3, m.get("l"));
+        assertEquals(ImmutableList.of("i", "j", "k", "l"), m.getKeys());
+    }
+
+    @Test
+    public void testSize5() {
+        StringToIndexMap m = StringToIndexMap.of(
+                "i", 0, "j", 1, "k", 2, "l", 3, "m", 4);
+        assertEquals(5, m.size());
+        assertEquals(-1, m.get("a"));
+        assertEquals(0, m.get("i"));
+        assertEquals(1, m.get("j"));
+        assertEquals(2, m.get("k"));
+        assertEquals(3, m.get("l"));
+        assertEquals(4, m.get("m"));
+        assertEquals(ImmutableList.of("i", "j", "k", "l", "m"), m.getKeys());
+    }
+
+    @Test
+    public void testSize6() {
+        StringToIndexMap m = StringToIndexMap.of(
+                "i", 0, "j", 1, "k", 2, "l", 3, "m", 4, "n", 5);
+        assertEquals(6, m.size());
+        assertEquals(-1, m.get("a"));
+        assertEquals(0, m.get("i"));
+        assertEquals(1, m.get("j"));
+        assertEquals(2, m.get("k"));
+        assertEquals(3, m.get("l"));
+        assertEquals(4, m.get("m"));
+        assertEquals(5, m.get("n"));
+        assertEquals(ImmutableList.of("i", "j", "k", "l", "m", "n"), m.getKeys());
+    }
+
+    @Test
+    public void testBufferWithExcessCapacity() {
+        StringToIndexMap m = StringToIndexMap.of(
+                new Entry[]{
+                        new Entry("i", 0), new Entry("j", 1), new Entry("k", 2),
+                        null, null, null, null
+                }, 3);
+        assertEquals(3, m.size());
+        assertEquals(-1, m.get("a"));
+        assertEquals(0, m.get("i"));
+        assertEquals(1, m.get("j"));
+        assertEquals(2, m.get("k"));
+        assertEquals(ImmutableList.of("i", "j", "k"), m.getKeys());
+    }
+
+    @Test
+    public void testSizeBig() {
+        // We try several sizes here, to catch rarely occurring bugs (needs a bucket of size 3 and such):
+        for (int size = 7; size <= 7 * 2 * 2 * 2 * 2 * 2 * 2 * 2; size *= 2) {
+            Entry[] entries = new Entry[size];
+            for (int i = 0; i < entries.length; i++) {
+                entries[i] = new Entry(
+                        "a" + i + "_" + i,  // This will hopefully generate some clashes
+                        i);
+            }
+
+            StringToIndexMap map = StringToIndexMap.of(entries);
+            assertEquals(size, map.size());
+            assertEquals(size, map.getKeys().size());
+            for (Entry entry : entries) {
+                assertEquals(entry.getValue(), map.get(entry.getKey()));
+
+                // Hoping for some matching buckets here:
+                for (int i = 0; i < 10; i++) {
+                    assertEquals(-1, map.get(i + "-" + entry.getKey()));
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testClashingKey() {
+        try {
+            StringToIndexMap.of("foo", 0, "foo", 1);
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("\"foo\""));
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/46c75010/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirDynamicCall.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirDynamicCall.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirDynamicCall.java
new file mode 100644
index 0000000..dea9bcd
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirDynamicCall.java
@@ -0,0 +1,439 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import org.apache.freemarker.core.model.CallPlace;
+import org.apache.freemarker.core.model.Constants;
+import org.apache.freemarker.core.model.TemplateCallableModel;
+import org.apache.freemarker.core.model.TemplateDirectiveModel2;
+import org.apache.freemarker.core.model.TemplateFunctionModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util.CommonSupplier;
+import org.apache.freemarker.core.util.StringToIndexMap;
+import org.apache.freemarker.core.util._StringUtil;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * AST directive node: {@code <@exp ...>}.
+ * Executes a {@link TemplateCallableModel} that's embedded directly into the static text. At least in the default
+ * template language the value must be a {@link TemplateDirectiveModel2}, though technically calling a
+ * {@link TemplateFunctionModel} is possible as well.
+ * <p>
+ * The {@link TemplateCallableModel} object is obtained on runtime by evaluating an expression, and the parameter list
+ * is also validated (how many positional parameters are allowed, what named parameters are supported) then. Hence, the
+ * call is "dynamic".
+ */
+class ASTDirDynamicCall extends ASTDirective implements CallPlace {
+
+    static final class NamedArgument {
+        private final String name;
+        private final ASTExpression value;
+
+        public NamedArgument(String name, ASTExpression value) {
+            this.name = name;
+            this.value = value;
+        }
+    }
+
+    private final ASTExpression callableValueExp;
+    private final ASTExpression[] positionalArgs;
+    private final NamedArgument[] namedArgs;
+    private final StringToIndexMap loopVarNames;
+    private final boolean allowCallingFunctions;
+
+    private CustomDataHolder customDataHolder;
+
+    /**
+     * @param allowCallingFunctions Some template languages may allow calling {@link TemplateFunctionModel}-s
+     *                              directly embedded into the static text, in which case this should be {@code true}.
+     */
+    ASTDirDynamicCall(
+            ASTExpression callableValueExp, boolean allowCallingFunctions,
+            ASTExpression[] positionalArgs, NamedArgument[] namedArgs, StringToIndexMap loopVarNames,
+            TemplateElements children) {
+        this.callableValueExp = callableValueExp;
+        this.allowCallingFunctions = allowCallingFunctions;
+
+        if (positionalArgs != null && positionalArgs.length == 0
+                || namedArgs != null && namedArgs.length == 0
+                || loopVarNames != null && loopVarNames.size() == 0) {
+            throw new IllegalArgumentException("Use null instead of empty collections");
+        }
+        this.positionalArgs = positionalArgs;
+        this.namedArgs = namedArgs;
+        this.loopVarNames = loopVarNames;
+
+        setChildren(children);
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException {
+        TemplateCallableModel callableValue;
+        TemplateDirectiveModel2 directive;
+        TemplateFunctionModel function;
+        {
+            TemplateModel callableValueTM = callableValueExp._eval(env);
+            if (callableValueTM instanceof TemplateDirectiveModel2) {
+                callableValue = (TemplateCallableModel) callableValueTM;
+                directive = (TemplateDirectiveModel2) callableValueTM;
+                function = null;
+            } else if (callableValueTM instanceof TemplateFunctionModel) {
+                if (!allowCallingFunctions) {
+                    // TODO [FM3][CF] Better exception
+                    throw new NonUserDefinedDirectiveLikeException(
+                            "Calling functions is not allowed on the top level in this template language", env);
+                }
+                callableValue = (TemplateCallableModel) callableValueTM;
+                directive = null;
+                function = (TemplateFunctionModel) callableValue;
+            } else if (callableValueTM == null) {
+                throw InvalidReferenceException.getInstance(callableValueExp, env);
+            } else {
+                throw new NonUserDefinedDirectiveLikeException(callableValueExp, callableValueTM, env);
+            }
+        }
+
+        int predefPosArgCnt = callableValue.getPredefinedPositionalArgumentCount();
+        boolean hasPosVarargsArg = callableValue.hasPositionalVarargsArgument();
+
+        if (positionalArgs != null && positionalArgs.length > predefPosArgCnt && !hasPosVarargsArg) {
+            throw new _MiscTemplateException(this,
+                    "The target callable ",
+                    (predefPosArgCnt != 0
+                        ? new Object[] { "can only have ", predefPosArgCnt }
+                        : "can't have"
+                    ),
+                    " arguments passed by position, but the invocation has ",
+                    positionalArgs.length, " such arguments.");
+        }
+
+        TemplateModel[] execArgs = new TemplateModel[callableValue.getTotalArgumentCount()];
+
+        // Fill predefined positional args:
+        if (positionalArgs != null) {
+            int actualPredefPosArgCnt = Math.min(positionalArgs.length, predefPosArgCnt);
+            for (int argIdx = 0; argIdx < actualPredefPosArgCnt; argIdx++) {
+                execArgs[argIdx] = positionalArgs[argIdx].eval(env);
+            }
+        }
+
+        if (hasPosVarargsArg) {
+            int posVarargCnt = positionalArgs != null ? positionalArgs.length - predefPosArgCnt : 0;
+            TemplateSequenceModel varargsSeq;
+            if (posVarargCnt <= 0) {
+                varargsSeq = Constants.EMPTY_SEQUENCE;
+            } else {
+                NativeSequence nativeSeq = new NativeSequence(posVarargCnt);
+                varargsSeq = nativeSeq;
+                for (int posVarargIdx = 0; posVarargIdx < posVarargCnt; posVarargIdx++) {
+                    nativeSeq.add(positionalArgs[predefPosArgCnt + posVarargIdx].eval(env));
+                }
+            }
+            execArgs[predefPosArgCnt] = varargsSeq;
+        }
+
+        int namedVarargsArgumentIndex = callableValue.getNamedVarargsArgumentIndex();
+        NativeHashEx2 namedVarargsHash = null;
+        if (namedArgs != null) {
+            for (NamedArgument namedArg : namedArgs) {
+                int argIdx = callableValue.getNamedArgumentIndex(namedArg.name);
+                if (argIdx != -1) {
+                    execArgs[argIdx] = namedArg.value.eval(env);
+                } else {
+                    if (namedVarargsHash == null) {
+                        if (namedVarargsArgumentIndex == -1) {
+                            Collection<String> validNames = callableValue.getPredefinedNamedArgumentNames();
+                            throw new _MiscTemplateException(this,
+                                    validNames == null || validNames.isEmpty()
+                                    ? new Object[] {
+                                            "The target callable doesn't have any by-name-passed parameters (like ",
+                                            new _DelayedJQuote(namedArg.name), ")"
+                                    }
+                                    : new Object[] {
+                                            "The target callable has no by-name-passed parameter called ",
+                                            new _DelayedJQuote(namedArg.name), ". The supported parameter names are:\n",
+                                            new _DelayedJQuotedListing(validNames)
+                                    });
+                        }
+                        namedVarargsHash = new NativeHashEx2();
+                    }
+                    namedVarargsHash.put(namedArg.name, namedArg.value.eval(env));
+                }
+            }
+        }
+        if (namedVarargsArgumentIndex != -1) {
+            execArgs[namedVarargsArgumentIndex] = namedVarargsHash != null ? namedVarargsHash : Constants.EMPTY_HASH;
+        }
+
+        if (directive != null) {
+            directive.execute(execArgs, env.getOut(), env, this);
+        } else {
+            TemplateModel result = function.execute(execArgs, env, this);
+            if (result == null) {
+                throw new _MiscTemplateException(this, "Function has returned no value (or null)");
+            }
+            // TODO [FM3][CF]
+            throw new BugException("Top-level function call not yet implemented");
+        }
+
+        return null;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return true;
+    }
+
+    @Override
+    boolean isShownInStackTrace() {
+        return true;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        StringBuilder sb = new StringBuilder();
+        if (canonical) sb.append('<');
+        sb.append('@');
+        MessageUtil.appendExpressionAsUntearable(sb, callableValueExp);
+        boolean nameIsInParen = sb.charAt(sb.length() - 1) == ')';
+        if (positionalArgs != null) {
+            for (int i = 0; i < positionalArgs.length; i++) {
+                ASTExpression argExp = (ASTExpression) positionalArgs[i];
+                if (i != 0) {
+                    sb.append(',');
+                }
+                sb.append(' ');
+                sb.append(argExp.getCanonicalForm());
+            }
+        }
+        if (namedArgs != null) {
+            for (NamedArgument namedArg : namedArgs) {
+                sb.append(' ');
+                sb.append(_StringUtil.toFTLTopLevelIdentifierReference(namedArg.name));
+                sb.append('=');
+                MessageUtil.appendExpressionAsUntearable(sb, namedArg.value);
+            }
+        }
+        if (loopVarNames != null) {
+            sb.append("; ");
+            boolean first = true;
+            for (String loopVarName : loopVarNames.getKeys()) {
+                if (!first) {
+                    sb.append(", ");
+                } else {
+                    first = false;
+                }
+                sb.append(_StringUtil.toFTLTopLevelIdentifierReference(loopVarName));
+            }
+        }
+        if (canonical) {
+            if (getChildCount() == 0) {
+                sb.append("/>");
+            } else {
+                sb.append('>');
+                sb.append(getChildrenCanonicalForm());
+                sb.append("</@");
+                if (!nameIsInParen
+                        && (callableValueExp instanceof ASTExpVariable
+                        || (callableValueExp instanceof ASTExpDot && ((ASTExpDot) callableValueExp).onlyHasIdentifiers()))) {
+                    sb.append(callableValueExp.getCanonicalForm());
+                }
+                sb.append('>');
+            }
+        }
+        return sb.toString();
+    }
+
+    @Override
+    String getASTNodeDescriptor() {
+        return "~";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1/*nameExp*/
+                + (positionalArgs != null ? positionalArgs.length : 0)
+                + (namedArgs != null ? namedArgs.length * 2 : 0)
+                + (loopVarNames != null ? loopVarNames.size() : 0);
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx == 0) {
+            return callableValueExp;
+        } else {
+            int base = 1;
+            final int positionalArgsSize = positionalArgs != null ? positionalArgs.length : 0;
+            if (idx - base < positionalArgsSize) {
+                return positionalArgs[idx - base];
+            } else {
+                base += positionalArgsSize;
+                final int namedArgsSize = namedArgs != null ? namedArgs.length : 0;
+                if (idx - base < namedArgsSize * 2) {
+                    NamedArgument namedArg = namedArgs[(idx - base) / 2];
+                    return (idx - base) % 2 == 0 ? namedArg.name : namedArg.value;
+                } else {
+                    base += namedArgsSize * 2;
+                    final int bodyParameterNamesSize = loopVarNames != null ? loopVarNames.size() : 0;
+                    if (idx - base < bodyParameterNamesSize) {
+                        return loopVarNames.getKeys().get(idx - base);
+                    } else {
+                        throw new IndexOutOfBoundsException();
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx == 0) {
+            return ParameterRole.CALLEE;
+        } else {
+            int base = 1;
+            final int positionalArgsSize = positionalArgs != null ? positionalArgs.length : 0;
+            if (idx - base < positionalArgsSize) {
+                return ParameterRole.ARGUMENT_VALUE;
+            } else {
+                base += positionalArgsSize;
+                final int namedArgsSize = namedArgs != null ? namedArgs.length : 0;
+                if (idx - base < namedArgsSize * 2) {
+                    return (idx - base) % 2 == 0 ? ParameterRole.ARGUMENT_NAME : ParameterRole.ARGUMENT_VALUE;
+                } else {
+                    base += namedArgsSize * 2;
+                    final int bodyParameterNamesSize = loopVarNames != null ? loopVarNames.size() : 0;
+                    if (idx - base < bodyParameterNamesSize) {
+                        return ParameterRole.TARGET_LOOP_VARIABLE;
+                    } else {
+                        throw new IndexOutOfBoundsException();
+                    }
+                }
+            }
+        }
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // CallPlace API:
+
+    @Override
+    public boolean hasNestedContent() {
+        return getChildCount() != 0;
+    }
+
+    @Override
+    public int getLoopVariableCount() {
+        return loopVarNames != null ? loopVarNames.size() : 0;
+    }
+
+    @Override
+    public void executeNestedContent(TemplateModel[] loopVariableValues, Environment env)
+            throws TemplateException, IOException {
+        env.visit(getChildBuffer(), loopVarNames, loopVariableValues);
+    }
+
+    @Override
+    @SuppressFBWarnings(value={ "IS2_INCONSISTENT_SYNC", "DC_DOUBLECHECK" }, justification="Performance tricks")
+    public Object getOrCreateCustomData(Object providerIdentity, CommonSupplier<?> supplier)
+            throws CallPlaceCustomDataInitializationException {
+        // We are using double-checked locking, utilizing Java memory model "final" trick.
+        // Note that this.customDataHolder is NOT volatile.
+
+        CustomDataHolder customDataHolder = this.customDataHolder;  // Findbugs false alarm
+        if (customDataHolder == null) {  // Findbugs false alarm
+            synchronized (this) {
+                customDataHolder = this.customDataHolder;
+                if (customDataHolder == null || customDataHolder.providerIdentity != providerIdentity) {
+                    customDataHolder = createNewCustomData(providerIdentity, supplier);
+                    this.customDataHolder = customDataHolder;
+                }
+            }
+        }
+
+        if (customDataHolder.providerIdentity != providerIdentity) {
+            synchronized (this) {
+                customDataHolder = this.customDataHolder;
+                if (customDataHolder == null || customDataHolder.providerIdentity != providerIdentity) {
+                    customDataHolder = createNewCustomData(providerIdentity, supplier);
+                    this.customDataHolder = customDataHolder;
+                }
+            }
+        }
+
+        return customDataHolder.customData;
+    }
+
+    private CustomDataHolder createNewCustomData(Object provierIdentity, CommonSupplier supplier)
+            throws CallPlaceCustomDataInitializationException {
+        CustomDataHolder customDataHolder;
+        Object customData;
+        try {
+            customData = supplier.get();
+        } catch (Exception e) {
+            throw new CallPlaceCustomDataInitializationException(
+                    "Failed to initialize custom data for provider identity "
+                            + _StringUtil.tryToString(provierIdentity) + " via factory "
+                            + _StringUtil.tryToString(supplier), e);
+        }
+        if (customData == null) {
+            throw new NullPointerException("CommonSupplier.get() has returned null");
+        }
+        customDataHolder = new CustomDataHolder(provierIdentity, customData);
+        return customDataHolder;
+    }
+
+    @Override
+    public boolean isNestedOutputCacheable() {
+        return isChildrenOutputCacheable();
+    }
+
+    @Override
+    public int getFirstTargetJavaParameterTypeIndex() {
+        // TODO [FM3]
+        return -1;
+    }
+
+    @Override
+    public Class<?> getTargetJavaParameterType(int argIndex) {
+        // TODO [FM3]
+        return null;
+    }
+
+    /**
+     * Used for implementing double check locking in implementing the
+     * {@link #getOrCreateCustomData(Object, CommonSupplier)}.
+     */
+    private static class CustomDataHolder {
+
+        private final Object providerIdentity;
+        private final Object customData;
+        public CustomDataHolder(Object providerIdentity, Object customData) {
+            this.providerIdentity = providerIdentity;
+            this.customData = customData;
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/46c75010/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirDynamicDirectiveCall.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirDynamicDirectiveCall.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirDynamicDirectiveCall.java
deleted file mode 100644
index 52ae027..0000000
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirDynamicDirectiveCall.java
+++ /dev/null
@@ -1,422 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.freemarker.core;
-
-import java.io.IOException;
-import java.util.Collection;
-
-import org.apache.freemarker.core.model.CallPlace;
-import org.apache.freemarker.core.model.Constants;
-import org.apache.freemarker.core.model.TemplateCallableModel;
-import org.apache.freemarker.core.model.TemplateDirectiveModel2;
-import org.apache.freemarker.core.model.TemplateFunctionModel;
-import org.apache.freemarker.core.model.TemplateModel;
-import org.apache.freemarker.core.model.TemplateSequenceModel;
-import org.apache.freemarker.core.util.BugException;
-import org.apache.freemarker.core.util.CommonSupplier;
-import org.apache.freemarker.core.util._StringUtil;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-class ASTDirDynamicDirectiveCall extends ASTDirective implements CallPlace {
-
-    static final class NamedArgument {
-        private final String name;
-        private final ASTExpression value;
-
-        public NamedArgument(String name, ASTExpression value) {
-            this.name = name;
-            this.value = value;
-        }
-    }
-
-    private final ASTExpression callableValueExp;
-    private final ASTExpression[] positionalArgs;
-    private final NamedArgument[] namedArgs;
-    private final String[] loopVarNames;
-    private final boolean allowCallingFunctions;
-
-    private CustomDataHolder customDataHolder;
-
-    /**
-     * @param allowCallingFunctions Some template languages may allow calling {@link TemplateFunctionModel}-s
-     *                              directly embedded into the static text, in which case this should be {@code true}.
-     */
-    ASTDirDynamicDirectiveCall(
-            ASTExpression callableValueExp, boolean allowCallingFunctions,
-            ASTExpression[] positionalArgs, NamedArgument[] namedArgs, String[] loopVarNames,
-            TemplateElements children) {
-        this.callableValueExp = callableValueExp;
-        this.allowCallingFunctions = allowCallingFunctions;
-
-        this.positionalArgs = positionalArgs;
-        this.namedArgs = namedArgs;
-        this.loopVarNames = loopVarNames;
-
-        setChildren(children);
-    }
-
-    @Override
-    ASTElement[] accept(Environment env) throws TemplateException, IOException {
-        TemplateCallableModel callableValue;
-        TemplateDirectiveModel2 directive;
-        TemplateFunctionModel function;
-        {
-            TemplateModel callableValueTM = callableValueExp._eval(env);
-            if (callableValueTM instanceof TemplateDirectiveModel2) {
-                callableValue = (TemplateCallableModel) callableValueTM;
-                directive = (TemplateDirectiveModel2) callableValueTM;
-                function = null;
-            } else if (callableValueTM instanceof TemplateFunctionModel) {
-                if (!allowCallingFunctions) {
-                    // TODO [FM3][CF] Better exception
-                    throw new NonUserDefinedDirectiveLikeException(
-                            "Calling functions is not allowed on the top level in this template language", env);
-                }
-                callableValue = (TemplateCallableModel) callableValueTM;
-                directive = null;
-                function = (TemplateFunctionModel) callableValue;
-            } else if (callableValueTM == null) {
-                throw InvalidReferenceException.getInstance(callableValueExp, env);
-            } else {
-                throw new NonUserDefinedDirectiveLikeException(callableValueExp, callableValueTM, env);
-            }
-        }
-
-        int predefPosArgCnt = callableValue.getPredefinedPositionalArgumentCount();
-        boolean hasPosVarargsArg = callableValue.hasPositionalVarargsArgument();
-
-        if (positionalArgs != null && positionalArgs.length > predefPosArgCnt && !hasPosVarargsArg) {
-            // TODO [FM3][CF] Better exception
-            throw new _MiscTemplateException(this,
-                    "The target callable ",
-                    (predefPosArgCnt != 0
-                        ? new Object[] { "can only have ", predefPosArgCnt }
-                        : "can't have"
-                    ),
-                    " arguments passed by position, but the invocation has ",
-                    positionalArgs.length, " such arguments.");
-        }
-
-        TemplateModel[] execArgs = new TemplateModel[callableValue.getTotalArgumentCount()];
-
-        // Fill predefined positional args:
-        if (positionalArgs != null) {
-            int actualPredefPosArgCnt = Math.min(positionalArgs.length, predefPosArgCnt);
-            for (int argIdx = 0; argIdx < actualPredefPosArgCnt; argIdx++) {
-                execArgs[argIdx] = positionalArgs[argIdx].eval(env);
-            }
-        }
-
-        if (hasPosVarargsArg) {
-            int posVarargCnt = positionalArgs != null ? positionalArgs.length - predefPosArgCnt : 0;
-            TemplateSequenceModel varargsSeq;
-            if (posVarargCnt <= 0) {
-                varargsSeq = Constants.EMPTY_SEQUENCE;
-            } else {
-                NativeSequence nativeSeq = new NativeSequence(posVarargCnt);
-                varargsSeq = nativeSeq;
-                for (int posVarargIdx = 0; posVarargIdx < posVarargCnt; posVarargIdx++) {
-                    nativeSeq.add(positionalArgs[predefPosArgCnt + posVarargIdx].eval(env));
-                }
-            }
-            execArgs[predefPosArgCnt] = varargsSeq;
-        }
-
-        int namedVarargsArgumentIndex = callableValue.getNamedVarargsArgumentIndex();
-        NativeHashEx2 namedVarargsHash = null;
-        if (namedArgs != null) {
-            for (NamedArgument namedArg : namedArgs) {
-                int argIdx = callableValue.getNamedArgumentIndex(namedArg.name);
-                if (argIdx != -1) {
-                    execArgs[argIdx] = namedArg.value.eval(env);
-                } else {
-                    if (namedVarargsHash == null) {
-                        if (namedVarargsArgumentIndex == -1) {
-                            Collection<String> validNames = callableValue.getPredefinedNamedArgumentNames();
-                            throw new _MiscTemplateException(this,
-                                    // TODO [FM3][CF] Better exception, esp. list the supported names
-                                    validNames == null || validNames.isEmpty()
-                                    ? new Object[] {
-                                            "The target callable doesn't have any by-name-passed parameters (like ",
-                                            new _DelayedJQuote(namedArg.name), ")"
-                                    }
-                                    : new Object[] {
-                                            "The target callable has no by-name-passed parameter called ",
-                                            new _DelayedJQuote(namedArg.name), ". The supported parameter names are:\n",
-                                            new _DelayedJQuotedListing(validNames)
-                                    });
-                        }
-                        namedVarargsHash = new NativeHashEx2();
-                    }
-                    namedVarargsHash.put(namedArg.name, namedArg.value.eval(env));
-                }
-            }
-        }
-        if (namedVarargsArgumentIndex != -1) {
-            execArgs[namedVarargsArgumentIndex] = namedVarargsHash != null ? namedVarargsHash : Constants.EMPTY_HASH;
-        }
-
-        if (directive != null) {
-            directive.execute(execArgs, env.getOut(), env, this);
-        } else {
-            TemplateModel result = function.execute(execArgs, env, this);
-            if (result == null) {
-                throw new _MiscTemplateException(this, "Function has returned no value (or null)");
-            }
-            // TODO [FM3][CF]
-            throw new BugException("Top-level function call not yet implemented");
-        }
-
-        return null;
-    }
-
-    @Override
-    boolean isNestedBlockRepeater() {
-        return true;
-    }
-
-    @Override
-    boolean isShownInStackTrace() {
-        return true;
-    }
-
-    @Override
-    protected String dump(boolean canonical) {
-        StringBuilder sb = new StringBuilder();
-        if (canonical) sb.append('<');
-        sb.append('~');
-        MessageUtil.appendExpressionAsUntearable(sb, callableValueExp);
-        boolean nameIsInParen = sb.charAt(sb.length() - 1) == ')';
-        if (positionalArgs != null) {
-            for (int i = 0; i < positionalArgs.length; i++) {
-                ASTExpression argExp = (ASTExpression) positionalArgs[i];
-                if (i != 0) {
-                    sb.append(',');
-                }
-                sb.append(' ');
-                sb.append(argExp.getCanonicalForm());
-            }
-        }
-        if (namedArgs != null) {
-            for (NamedArgument namedArg : namedArgs) {
-                sb.append(' ');
-                sb.append(_StringUtil.toFTLTopLevelIdentifierReference(namedArg.name));
-                sb.append('=');
-                MessageUtil.appendExpressionAsUntearable(sb, namedArg.value);
-            }
-        }
-        if (loopVarNames != null && loopVarNames.length != 0) {
-            sb.append("; ");
-            for (int i = 0; i < loopVarNames.length; i++) {
-                if (i != 0) {
-                    sb.append(", ");
-                }
-                sb.append(_StringUtil.toFTLTopLevelIdentifierReference((String) loopVarNames[i]));
-            }
-        }
-        if (canonical) {
-            if (getChildCount() == 0) {
-                sb.append("/>");
-            } else {
-                sb.append('>');
-                sb.append(getChildrenCanonicalForm());
-                sb.append("</~");
-                if (!nameIsInParen
-                        && (callableValueExp instanceof ASTExpVariable
-                        || (callableValueExp instanceof ASTExpDot && ((ASTExpDot) callableValueExp).onlyHasIdentifiers()))) {
-                    sb.append(callableValueExp.getCanonicalForm());
-                }
-                sb.append('>');
-            }
-        }
-        return sb.toString();
-    }
-
-    @Override
-    String getASTNodeDescriptor() {
-        return "~";
-    }
-
-    @Override
-    int getParameterCount() {
-        return 1/*nameExp*/
-                + (positionalArgs != null ? positionalArgs.length : 0)
-                + (namedArgs != null ? namedArgs.length * 2 : 0)
-                + (loopVarNames != null ? loopVarNames.length : 0);
-    }
-
-    @Override
-    Object getParameterValue(int idx) {
-        if (idx == 0) {
-            return callableValueExp;
-        } else {
-            int base = 1;
-            final int positionalArgsSize = positionalArgs != null ? positionalArgs.length : 0;
-            if (idx - base < positionalArgsSize) {
-                return positionalArgs[idx - base];
-            } else {
-                base += positionalArgsSize;
-                final int namedArgsSize = namedArgs != null ? namedArgs.length : 0;
-                if (idx - base < namedArgsSize * 2) {
-                    NamedArgument namedArg = namedArgs[(idx - base) / 2];
-                    return (idx - base) % 2 == 0 ? namedArg.name : namedArg.value;
-                } else {
-                    base += namedArgsSize * 2;
-                    final int bodyParameterNamesSize = loopVarNames != null ? loopVarNames.length : 0;
-                    if (idx - base < bodyParameterNamesSize) {
-                        return loopVarNames[idx - base];
-                    } else {
-                        throw new IndexOutOfBoundsException();
-                    }
-                }
-            }
-        }
-    }
-
-    @Override
-    ParameterRole getParameterRole(int idx) {
-        if (idx == 0) {
-            return ParameterRole.CALLEE;
-        } else {
-            int base = 1;
-            final int positionalArgsSize = positionalArgs != null ? positionalArgs.length : 0;
-            if (idx - base < positionalArgsSize) {
-                return ParameterRole.ARGUMENT_VALUE;
-            } else {
-                base += positionalArgsSize;
-                final int namedArgsSize = namedArgs != null ? namedArgs.length : 0;
-                if (idx - base < namedArgsSize * 2) {
-                    return (idx - base) % 2 == 0 ? ParameterRole.ARGUMENT_NAME : ParameterRole.ARGUMENT_VALUE;
-                } else {
-                    base += namedArgsSize * 2;
-                    final int bodyParameterNamesSize = loopVarNames != null ? loopVarNames.length : 0;
-                    if (idx - base < bodyParameterNamesSize) {
-                        return ParameterRole.TARGET_LOOP_VARIABLE;
-                    } else {
-                        throw new IndexOutOfBoundsException();
-                    }
-                }
-            }
-        }
-    }
-
-    // -----------------------------------------------------------------------------------------------------------------
-    // CallPlace API:
-
-    @Override
-    public boolean hasNestedContent() {
-        return getChildCount() != 0;
-    }
-
-    @Override
-    public int getLoopVariableCount() {
-        return loopVarNames != null ? loopVarNames.length : 0;
-    }
-
-    @Override
-    public void executeNestedContent(TemplateModel[] loopVariableValues, Environment env) throws TemplateException {
-        // TODO Automatically generated method
-        throw new BugException("Not implemented");
-    }
-
-    @Override
-    @SuppressFBWarnings(value={ "IS2_INCONSISTENT_SYNC", "DC_DOUBLECHECK" }, justification="Performance tricks")
-    public Object getOrCreateCustomData(Object providerIdentity, CommonSupplier supplier)
-            throws CallPlaceCustomDataInitializationException {
-        // We are using double-checked locking, utilizing Java memory model "final" trick.
-        // Note that this.customDataHolder is NOT volatile.
-
-        CustomDataHolder customDataHolder = this.customDataHolder;  // Findbugs false alarm
-        if (customDataHolder == null) {  // Findbugs false alarm
-            synchronized (this) {
-                customDataHolder = this.customDataHolder;
-                if (customDataHolder == null || customDataHolder.providerIdentity != providerIdentity) {
-                    customDataHolder = createNewCustomData(providerIdentity, supplier);
-                    this.customDataHolder = customDataHolder;
-                }
-            }
-        }
-
-        if (customDataHolder.providerIdentity != providerIdentity) {
-            synchronized (this) {
-                customDataHolder = this.customDataHolder;
-                if (customDataHolder == null || customDataHolder.providerIdentity != providerIdentity) {
-                    customDataHolder = createNewCustomData(providerIdentity, supplier);
-                    this.customDataHolder = customDataHolder;
-                }
-            }
-        }
-
-        return customDataHolder.customData;
-    }
-
-    private CustomDataHolder createNewCustomData(Object provierIdentity, CommonSupplier supplier)
-            throws CallPlaceCustomDataInitializationException {
-        CustomDataHolder customDataHolder;
-        Object customData;
-        try {
-            customData = supplier.get();
-        } catch (Exception e) {
-            throw new CallPlaceCustomDataInitializationException(
-                    "Failed to initialize custom data for provider identity "
-                            + _StringUtil.tryToString(provierIdentity) + " via factory "
-                            + _StringUtil.tryToString(supplier), e);
-        }
-        if (customData == null) {
-            throw new NullPointerException("CommonSupplier.get() has returned null");
-        }
-        customDataHolder = new CustomDataHolder(provierIdentity, customData);
-        return customDataHolder;
-    }
-
-    @Override
-    public boolean isNestedOutputCacheable() {
-        return isChildrenOutputCacheable();
-    }
-
-    @Override
-    public int getFirstTargetJavaParameterTypeIndex() {
-        // TODO [FM3]
-        return -1;
-    }
-
-    @Override
-    public Class<?> getTargetJavaParameterType(int argIndex) {
-        // TODO [FM3]
-        return null;
-    }
-
-    /**
-     * Used for implementing double check locking in implementing the
-     * {@link #getOrCreateCustomData(Object, CommonSupplier)}.
-     */
-    private static class CustomDataHolder {
-
-        private final Object providerIdentity;
-        private final Object customData;
-        public CustomDataHolder(Object providerIdentity, Object customData) {
-            this.providerIdentity = providerIdentity;
-            this.customData = customData;
-        }
-
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/46c75010/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java b/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
index 28b85f4..a47a83c 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
@@ -63,6 +63,7 @@ import org.apache.freemarker.core.model.impl.SimpleHash;
 import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException;
 import org.apache.freemarker.core.templateresolver.TemplateResolver;
 import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormat;
+import org.apache.freemarker.core.util.StringToIndexMap;
 import org.apache.freemarker.core.util.UndeclaredThrowableException;
 import org.apache.freemarker.core.util._DateUtil;
 import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory;
@@ -489,7 +490,34 @@ public final class Environment extends MutableProcessingConfiguration<Environmen
             directiveModel.execute(this, args, outArgs, nested);
         } finally {
             if (outArgs.length > 0) {
-                localContextStack.pop();
+                popLocalContext();
+            }
+        }
+    }
+
+    void visit(
+            ASTElement[] childBuffer,
+            final StringToIndexMap loopVarNames, final TemplateModel[] loopVarValues)
+            throws IOException, TemplateException {
+        if (loopVarNames == null) {
+            visit(childBuffer);
+        } else {
+            pushLocalContext(new LocalContext() {
+                @Override
+                public TemplateModel getLocalVariable(String name) throws TemplateModelException {
+                    int index = loopVarNames.get(name);
+                    return index != -1 ? loopVarValues[index] : null;
+                }
+
+                @Override
+                public Collection getLocalVariableNames() throws TemplateModelException {
+                    return loopVarNames.getKeys();
+                }
+            });
+            try {
+                visit(childBuffer);
+            } finally {
+                popLocalContext();
             }
         }
     }
@@ -619,7 +647,7 @@ public final class Environment extends MutableProcessingConfiguration<Environmen
                 visit(nestedContentBuffer);
             } finally {
                 if (invokingMacroContext.nestedContentParameterNames != null) {
-                    localContextStack.pop();
+                    popLocalContext();
                 }
                 currentMacroContext = invokingMacroContext;
                 currentNamespace = getMacroNamespace(invokingMacroContext.getMacro());
@@ -640,7 +668,7 @@ public final class Environment extends MutableProcessingConfiguration<Environmen
             handleTemplateException(te);
             return true;
         } finally {
-            localContextStack.pop();
+            popLocalContext();
         }
     }
 
@@ -2348,6 +2376,10 @@ public final class Environment extends MutableProcessingConfiguration<Environmen
         localContextStack.push(localContext);
     }
 
+    private void popLocalContext() {
+        localContextStack.pop();
+    }
+
     LocalContextStack getLocalContextStack() {
         return localContextStack;
     }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/46c75010/freemarker-core/src/main/java/org/apache/freemarker/core/model/CallPlace.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/CallPlace.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/CallPlace.java
index 1f131a3..f4f3f57 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/model/CallPlace.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/CallPlace.java
@@ -19,6 +19,7 @@
 
 package org.apache.freemarker.core.model;
 
+import java.io.IOException;
 import java.util.IdentityHashMap;
 
 import org.apache.freemarker.core.CallPlaceCustomDataInitializationException;
@@ -59,7 +60,8 @@ public interface CallPlace {
      *         TemplateCallableModelUtils#EMPTY_TEMPLATE_MODEL_ARRAY}. Its length must be equal to
      *         {@link #getLoopVariableCount()}.
      */
-    void executeNestedContent(TemplateModel[] loopVariableValues, Environment env) throws TemplateException;
+    void executeNestedContent(TemplateModel[] loopVariableValues, Environment env) throws TemplateException,
+            IOException;
 
     // -------------------------------------------------------------------------------------------------------------
     // Source code info:
@@ -135,7 +137,7 @@ public interface CallPlace {
      * @throws CallPlaceCustomDataInitializationException
      *             If the {@link CommonSupplier} had to be invoked but failed.
      */
-    Object getOrCreateCustomData(Object providerIdentity, CommonSupplier supplier)
+    Object getOrCreateCustomData(Object providerIdentity, CommonSupplier<?> supplier)
             throws CallPlaceCustomDataInitializationException;
 
     /**

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/46c75010/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCallableModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCallableModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCallableModel.java
index bc053bd..e20d73a 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCallableModel.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCallableModel.java
@@ -21,6 +21,8 @@ package org.apache.freemarker.core.model;
 
 import java.util.Collection;
 
+import org.apache.freemarker.core.util.StringToIndexMap;
+
 /**
  * Super interface of {@link TemplateFunctionModel} and {@link TemplateDirectiveModel2}.
  */
@@ -48,6 +50,7 @@ public interface TemplateCallableModel extends TemplateModel {
 
     /**
      * Returns if with what array index should the given named argument by passed to the {@code execute} method.
+     * Consider using a static final {@link StringToIndexMap} field to implement this.
      *
      * @return -1 if there's no such named argument
      */

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/46c75010/freemarker-core/src/main/java/org/apache/freemarker/core/util/StringToIndexMap.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/StringToIndexMap.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/StringToIndexMap.java
new file mode 100644
index 0000000..9c7dd9c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/StringToIndexMap.java
@@ -0,0 +1,316 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.util;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Maps string keys to non-negative int-s. This isn't a {@link Map}, but a more specialized class. It's immutable.
+ * It's slower to create than {@link HashMap}, but usually is a bit faster to read, and when {@link HashMap} gets
+ * unlucky with clashing hash keys, then it can be significantly faster.
+ */
+public final class StringToIndexMap {
+
+    public static final StringToIndexMap EMPTY = new StringToIndexMap();
+
+    private static final int MAX_VARIATIONS_TRIED = 4;
+
+    private final Entry[] buckets;
+    private final int bucketIndexMask;
+    private final int bucketIndexOverlap;
+    /** Attention: null when you have exactly 1 key, as then there's no key order to remember. */
+    private final List<String> keys;
+
+    private static class BucketsSetup {
+        private final Entry[] buckets;
+        private final int bucketIndexMask;
+        private final int bucketIndexOverlap;
+
+        public BucketsSetup(Entry[] buckets, int bucketIndexMask, int bucketIndexOverlap) {
+            this.buckets = buckets;
+            this.bucketIndexMask = bucketIndexMask;
+            this.bucketIndexOverlap = bucketIndexOverlap;
+        }
+    }
+
+    /**
+     * Convenience method for calling {@link #of(Entry[])} with 1 entry.
+     */
+    public static StringToIndexMap of(String key, int value) {
+        // Call special case constructor
+        return new StringToIndexMap(new Entry(key, value));
+    }
+
+    /**
+     * Convenience method for calling {@link #of(Entry[])} with 2 entries.
+     */
+    public static StringToIndexMap of(String k1, int v1, String k2, int v2) {
+        return of(new Entry(k1, v1), new Entry(k2, v2));
+    }
+
+    /**
+     * Convenience method for calling {@link #of(Entry[])} with 3 entries.
+     */
+    public static StringToIndexMap of(String k1, int v1, String k2, int v2, String k3, int v3) {
+        return of(new Entry(k1, v1), new Entry(k2, v2), new Entry(k3, v3));
+    }
+
+    /**
+     * Convenience method for calling {@link #of(Entry[])} with 4 entries.
+     */
+    public static StringToIndexMap of(String k1, int v1, String k2, int v2, String k3, int v3, String k4, int v4) {
+        return of(new Entry(k1, v1), new Entry(k2, v2), new Entry(k3, v3), new Entry(k4, v4));
+    }
+
+    /**
+     * Convenience method for calling {@link #of(Entry[])} with 5 entries.
+     */
+    public static StringToIndexMap of(String k1, int v1, String k2, int v2, String k3, int v3, String k4, int v4,
+            String k5, int v5) {
+        return of(new Entry(k1, v1), new Entry(k2, v2), new Entry(k3, v3), new Entry(k4, v4), new Entry(k5, v5));
+    }
+
+    /**
+     * Convenience method for calling {@link #of(Entry[])} with 6 entries.
+     */
+    public static StringToIndexMap of(String k1, int v1, String k2, int v2, String k3, int v3, String k4, int v4,
+            String k5, int v5, String k6, int v6) {
+        return of(new Entry(k1, v1), new Entry(k2, v2), new Entry(k3, v3), new Entry(k4, v4), new Entry(k5, v5),
+                new Entry(k6, v6));
+    }
+
+    /**
+     * @param entries Contains the entries that will be copied into the created object.
+     * @param entriesLength The method assumes that we only have this many elements; the {@code entries} parameter
+     *                      array might be longer than this.
+     */
+    public static StringToIndexMap of(Entry[] entries, int entriesLength) {
+        return entriesLength == 0 ? EMPTY
+                : entriesLength == 1 ? new StringToIndexMap(entries[0])
+                : new StringToIndexMap(entries, entriesLength);
+    }
+
+    /**
+     * Same as {@link #of(Entry[], int)}, with the last parameter set to {@code entries.length}.
+     */
+    public static StringToIndexMap of(Entry... entries) {
+        return of(entries, entries.length);
+    }
+
+    // The 0 argument case is weird, but because of {@link #of(Entry... entries)} it's possible even without this.
+    public static StringToIndexMap of() {
+        return EMPTY;
+    }
+
+    // This is a very frequent case, so we optimize for it a bit.
+    private StringToIndexMap(Entry entry) {
+        buckets = new Entry[] { entry };
+        bucketIndexMask = 0;
+        bucketIndexOverlap = 0;
+        keys = null; // As there's only one key, we can extract the key list from the buckets.
+    }
+
+    private StringToIndexMap(Entry... entries) {
+        this(entries, entries.length);
+    }
+
+    private StringToIndexMap(Entry[] entries, int entriesLength) {
+        if (entriesLength == 0) {
+            buckets = null;
+            bucketIndexMask = 0;
+            bucketIndexOverlap = 0;
+            keys = Collections.emptyList();
+        } else {
+            String[] keyArray = new String[entriesLength];
+            for (int i = 0; i < entriesLength; i++) {
+                keyArray[i] = entries[i].key;
+            }
+            keys = new _ArrayList<>(keyArray);
+
+            // We try to find the best hash algorithm parameter (variation) for the known key set:
+            int variation = 0;
+            BucketsSetup bestSetup = null;
+            int bestSetupGoodness = 0;
+            do {
+                variation++;
+                BucketsSetup setup = getBucketsSetup(entries, entriesLength, variation);
+
+                int filledBucketCnt = 0;
+                for (Entry bucket : setup.buckets) {
+                    if (bucket != null) {
+                        filledBucketCnt++;
+                    }
+                }
+                // Ideally, filledBucketCnt == entriesLength. If less, we have buckets with more then 1 element.
+                int setupGoodness = filledBucketCnt - entriesLength;
+
+                if (bestSetup == null || bestSetupGoodness < setupGoodness) {
+                    bestSetup = setup;
+                    bestSetupGoodness = setupGoodness;
+                }
+            } while (bestSetupGoodness != 0 && variation < MAX_VARIATIONS_TRIED);
+
+            this.buckets = bestSetup.buckets;
+            this.bucketIndexMask = bestSetup.bucketIndexMask;
+            this.bucketIndexOverlap = bestSetup.bucketIndexOverlap;
+        }
+    }
+
+    private static BucketsSetup getBucketsSetup(Entry[] entries, int entriesLength, int variation) {
+        // The 2.5 multipier was chosen experimentally.
+        int bucketCnt = getPowerOf2GreaterThanOrEqualTo(entriesLength * 2 + entriesLength / 2);
+
+        Entry[] buckets = new Entry[bucketCnt];
+        int bucketIndexMask = bucketCnt - 1;
+        int bucketIndexOverlap = Integer.numberOfTrailingZeros(bucketCnt) + (variation - 1);
+        for (int i = 0; i < entriesLength; i++) {
+            Entry entry = entries[i];
+            int bucketIdx = getBucketIndex(entry.key, bucketIndexMask, bucketIndexOverlap);
+            Entry nextInSameBucket = buckets[bucketIdx];
+            checkNameUnique(nextInSameBucket, entry.key);
+            buckets[bucketIdx] = nextInSameBucket != entry.nextInSameBucket
+                    ? new Entry(entry.key, entry.value, nextInSameBucket)
+                    : entry;
+        }
+        return new BucketsSetup(buckets, bucketIndexMask, bucketIndexOverlap);
+    }
+
+    private static int getBucketIndex(String key, int bucketIndexMask, int bucketIndexOverlap) {
+        int h = key.hashCode();
+        // What's the shift+xor trick: When we just drop the higher bits with plain masking, for some inputs the
+        // distribution among the buckets weren't uniform at all. For example, "k11", "k22", "k33", "k44", etc. all
+        // fell into the same bucket. To make such clashes less likely, we use twice as much bits as the mask size,
+        // by xor-ring the next mask-size-number-of-bits on the hash. That's still far from all the bits, but it seems
+        // it's good enough, and overlapping with all the bit would be slower.
+        // Note: This is all based of the String.hashCode() of Java 8 though... let's hope it won't get worse.
+        return (h ^ (h >>> bucketIndexOverlap)) & bucketIndexMask;
+    }
+
+    /**
+     * Returns the integer mapped to the name, or -1 if nothing is mapped to the int.
+     */
+    public int get(String key) {
+        if (buckets == null) {
+            return -1;
+        }
+
+        Entry entry = buckets[getBucketIndex(key, bucketIndexMask, bucketIndexOverlap)];
+        if (entry == null) {
+            return -1;
+        }
+        while (!entry.key.equals(key)) {
+            entry = entry.nextInSameBucket;
+            if (entry == null) {
+                return -1;
+            }
+        }
+        return entry.value;
+    }
+
+    /**
+     * Return the keys in the order as they were once specified. This is not necessary a very fast operation (as it's
+     * meant to be used for error message generation, to show valid names).
+     */
+    public List<String> getKeys() {
+        return keys != null ? keys : Collections.singletonList(buckets[0].key);
+    }
+
+    public int size() {
+        return keys != null ? keys.size() : 1;
+    }
+
+    private static int getPowerOf2GreaterThanOrEqualTo(int n) {
+        if (n == 0) {
+            return 0;
+        }
+
+        int powOf2 = 1;
+        while (powOf2 < n) {
+            powOf2 = (powOf2 << 1);
+        }
+        return powOf2;
+    }
+
+    private static void checkNameUnique(Entry entry, String key) {
+        while (entry != null) {
+            if (entry.key.equals(key)) {
+                throw new IllegalArgumentException("Duplicate key: " + _StringUtil.jQuote(key));
+            }
+            entry = entry.nextInSameBucket;
+        }
+    }
+
+    public static class Entry {
+        private final String key;
+        private final int value;
+        private final Entry nextInSameBucket;
+
+        public Entry(String key, int value) {
+            this(key, value, null);
+        }
+
+        private Entry(String key, int value, Entry nextInSameBucket) {
+            this.key = key;
+            this.value = value;
+            this.nextInSameBucket = nextInSameBucket;
+        }
+
+        public String getKey() {
+            return key;
+        }
+
+        public int getValue() {
+            return value;
+        }
+    }
+
+    /*
+    // Code used to see how well the elements are spread among the buckets:
+
+    void dumpBucketSizes() {
+        Map<Integer, Integer> hist = new TreeMap<>();
+        int goodness = -size();
+        for (Entry bucket : buckets) {
+            int bucketSize = bucketSize(bucket);
+            if (bucketSize != 0) {
+                goodness++;
+            }
+            Integer histCnt = hist.get(bucketSize);
+            hist.put(bucketSize, histCnt == null ? 1 : histCnt + 1);
+            System.out.print(bucketSize);
+        }
+        System.out.println();
+        System.out.println(hist + "; goodness " + goodness);
+    }
+
+    private int bucketSize(Entry entry) {
+        int size = 0;
+        while (entry != null) {
+            size++;
+            entry = entry.nextInSameBucket;
+        }
+        return size;
+    }
+    */
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/46c75010/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayList.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayList.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayList.java
new file mode 100644
index 0000000..5e1095e
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ArrayList.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.util;
+
+import java.util.AbstractList;
+import java.util.Arrays;
+import java.util.Iterator;
+
+/**
+ * Don't use this; used internally by FreeMarker, might changes without notice.
+ * Immutable list that wraps an array that's known to be non-changing.
+ */
+public class _ArrayList<E> extends AbstractList<E> {
+
+    private final E[] array;
+
+    public _ArrayList(E[] array) {
+        this.array = array;
+    }
+
+    @Override
+    public int size() {
+        return array.length;
+    }
+
+    @Override
+    public E get(int index) {
+        return array[index];
+    }
+
+    @Override
+    public Iterator<E> iterator() {
+        return new _ArrayIterator(array);
+    }
+
+    @Override
+    public Object[] toArray() {
+        return Arrays.copyOf(array, array.length);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/46c75010/freemarker-core/src/main/javacc/FTL.jj
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/javacc/FTL.jj b/freemarker-core/src/main/javacc/FTL.jj
index 97e2ade..c42c7a9 100644
--- a/freemarker-core/src/main/javacc/FTL.jj
+++ b/freemarker-core/src/main/javacc/FTL.jj
@@ -35,7 +35,7 @@ import org.apache.freemarker.core.outputformat.impl.*;
 import org.apache.freemarker.core.model.*;
 import org.apache.freemarker.core.model.impl.*;
 import org.apache.freemarker.core.util.*;
-import org.apache.freemarker.core.ASTDirDynamicDirectiveCall.NamedArgument;
+import org.apache.freemarker.core.ASTDirDynamicCall.NamedArgument;
 import java.io.*;
 import java.util.*;
 import java.nio.charset.Charset;
@@ -103,7 +103,7 @@ public class FMParser {
     private NamedArgument[] topNamedArgsBuffer;
     private int topNamedArgsLength;
     private static final int INITAL_TOP_LOOP_VAR_NAMES_BUFFER_SIZE = 4;
-    private String[] topLoopVarNamesBuffer;
+    private StringToIndexMap.Entry[] topLoopVarNamesBuffer;
     private int topLoopVarNamesLength;
 
     FMParser(Template template, Reader reader,
@@ -450,15 +450,16 @@ public class FMParser {
 
     private void addToTopLoopVarNames(String loopVarName) {
         if (topLoopVarNamesBuffer == null) {
-            topLoopVarNamesBuffer = new String[INITAL_TOP_LOOP_VAR_NAMES_BUFFER_SIZE];
+            topLoopVarNamesBuffer = new StringToIndexMap.Entry[INITAL_TOP_LOOP_VAR_NAMES_BUFFER_SIZE];
         } else if (topLoopVarNamesBuffer.length == topLoopVarNamesLength) {
-            String[] newLoopVarNamesBuffer =  new String[topLoopVarNamesBuffer.length * 2];
+            StringToIndexMap.Entry[] newLoopVarsBuffer = new StringToIndexMap.Entry[topLoopVarNamesBuffer.length * 2];
             for (int i = 0; i < topLoopVarNamesBuffer.length; i++) {
-                newLoopVarNamesBuffer[i] = topLoopVarNamesBuffer[i];
+                newLoopVarsBuffer[i] = topLoopVarNamesBuffer[i];
             }
-            topLoopVarNamesBuffer = newLoopVarNamesBuffer;
+            topLoopVarNamesBuffer = newLoopVarsBuffer;
         }
-        topLoopVarNamesBuffer[topLoopVarNamesLength++] = loopVarName;
+        topLoopVarNamesBuffer[topLoopVarNamesLength] = new StringToIndexMap.Entry(loopVarName, topLoopVarNamesLength);
+        topLoopVarNamesLength++;
     }
 
 }
@@ -3242,15 +3243,8 @@ ASTElement DynamicDirectiveCall() :
             }
         }
 
-        String[] trimmedLoopVarNames;
-        if (topLoopVarNamesLength == 0) {
-            trimmedLoopVarNames = null;
-        } else {
-            trimmedLoopVarNames = new String[topLoopVarNamesLength];
-            for (int i = 0; i < topLoopVarNamesLength; i++) {
-                trimmedLoopVarNames[i] = topLoopVarNamesBuffer[i];
-            }
-        }
+        StringToIndexMap loopVarNames = topLoopVarNamesLength == 0 ? null
+                : StringToIndexMap.of(topLoopVarNamesBuffer, topLoopVarNamesLength);
     }
 
     (
@@ -3263,7 +3257,7 @@ ASTElement DynamicDirectiveCall() :
                     // It's possible that we shadow a #list/#items loop variable, in which case that must be noted.
                     int ctxsLen = iteratorBlockContexts.size();
 	                for (int loopVarIdx = 0; loopVarIdx < topLoopVarNamesLength; loopVarIdx++) {
-                        String loopVarName = (String) topLoopVarNamesBuffer[loopVarIdx];
+                        String loopVarName = topLoopVarNamesBuffer[loopVarIdx].getKey();
                         walkCtxStack: for (int ctxIdx = ctxsLen - 1; ctxIdx >= 0; ctxIdx--) {
                             ParserIteratorBlockContext ctx
                                     = (ParserIteratorBlockContext) iteratorBlockContexts.get(ctxIdx);
@@ -3307,9 +3301,9 @@ ASTElement DynamicDirectiveCall() :
         )
     )
     {
-        ASTElement result = new ASTDirDynamicDirectiveCall(
+        ASTElement result = new ASTDirDynamicCall(
                 callableValueExp, false,
-                trimmedPositionalArgs, trimmedNamedArgs, trimmedLoopVarNames,
+                trimmedPositionalArgs, trimmedNamedArgs, loopVarNames,
                 children);
         result.setLocation(template, start, end);
         return result;