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:04 UTC

[02/21] incubator-freemarker git commit: FREEMARKER-63: Very early state. Until it's fully functional, the new interface is called TemplateDirectiveModel2, and is invoked with <~...> instead of <@...>. Later it will replace TemplateDirectiveModel and th

FREEMARKER-63:  Very early state. Until it's fully functional, the new interface is called TemplateDirectiveModel2, and is invoked with <~...> instead of <@...>. Later it will replace TemplateDirectiveModel and the syntax will be <@...>.


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

Branch: refs/heads/3
Commit: c28a78bd8dad4bfd3a37a90a26fbb15639d33604
Parents: 4295e24
Author: ddekany <dd...@apache.org>
Authored: Mon Jul 24 20:26:29 2017 +0200
Committer: ddekany <dd...@apache.org>
Committed: Tue Jul 25 00:39:22 2017 +0200

----------------------------------------------------------------------
 .../core/TemplateCallableModelTest.java         | 130 ++++++
 .../freemarker/core/model/ConstantsTest.java    |  21 +
 .../core/userpkg/AllFeaturesDirective.java      | 133 ++++++
 .../userpkg/TestTemplateDirectiveModel.java     |  88 ++++
 .../core/userpkg/TwoNamedParamsDirective.java   |  85 ++++
 .../userpkg/TwoPositionalParamsDirective.java   |  71 ++++
 .../core/ASTDirDynamicDirectiveCall.java        | 422 +++++++++++++++++++
 .../freemarker/core/NonNumericalException.java  |  12 +-
 .../NonUserDefinedDirectiveLikeException.java   |   1 +
 .../core/TemplateCallableModelUtils.java        |  48 +++
 .../core/UnexpectedTypeException.java           |  69 ++-
 .../freemarker/core/_DelayedJQuotedListing.java |  46 ++
 .../apache/freemarker/core/model/CallPlace.java | 173 ++++++++
 .../apache/freemarker/core/model/Constants.java |  36 +-
 .../core/model/TemplateCallableModel.java       |  86 ++++
 .../core/model/TemplateDirectiveModel2.java     |  28 ++
 .../core/model/TemplateFunctionModel.java       |  25 ++
 .../apache/freemarker/core/util/FTLUtil.java    |   1 +
 freemarker-core/src/main/javacc/FTL.jj          | 309 ++++++++++++--
 19 files changed, 1729 insertions(+), 55 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/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
new file mode 100644
index 0000000..67b73d0
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateCallableModelTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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 org.apache.freemarker.core.userpkg.AllFeaturesDirective;
+import org.apache.freemarker.core.userpkg.TwoNamedParamsDirective;
+import org.apache.freemarker.core.userpkg.TwoPositionalParamsDirective;
+import org.apache.freemarker.test.TemplateTest;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TemplateCallableModelTest extends TemplateTest {
+
+    @Before
+    public void addCommonData() {
+        addToDataModel("a", new AllFeaturesDirective());
+        addToDataModel("p", new TwoPositionalParamsDirective());
+        addToDataModel("n", new TwoNamedParamsDirective());
+    }
+
+    @Test
+    public void testBasicCall() throws IOException, TemplateException {
+        assertOutput("<~p />",
+                "#p(p1=null, p2=null)");
+        assertOutput("<~p 1 />",
+                "#p(p1=1, p2=null)");
+        assertOutput("<~p 1, 2 />",
+                "#p(p1=1, p2=2)");
+
+        assertOutput("<~n />",
+                "#n(n1=null, n2=null)");
+        assertOutput("<~n n1=11/>",
+                "#n(n1=11, n2=null)");
+        assertOutput("<~n n1=11 n2=22/>",
+                "#n(n1=11, n2=22)");
+
+        assertOutput("<~a />",
+                "#a(p1=null, p2=null, pOthers=[], n1=null, n2=null, nOthers={})");
+        assertOutput("<~a 1, 2 />",
+                "#a(p1=1, p2=2, pOthers=[], n1=null, n2=null, nOthers={})");
+        assertOutput("<~a n1=11 n2=22 />",
+                "#a(p1=null, p2=null, pOthers=[], n1=11, n2=22, nOthers={})");
+
+        assertOutput("<~a 1, 2 n1=11 n2=22 />",
+                "#a(p1=1, p2=2, pOthers=[], n1=11, n2=22, nOthers={})");
+        assertOutput("<~a 1 n1=11 />",
+                "#a(p1=1, p2=null, pOthers=[], n1=11, n2=null, nOthers={})");
+        assertOutput("<~a 1, 2, 3 n1=11 n2=22 n3=33 />",
+                "#a(p1=1, p2=2, pOthers=[3], n1=11, n2=22, nOthers={\"n3\": 33})");
+        assertOutput("<~a 1 n1=11 n3=33 />",
+                "#a(p1=1, p2=null, pOthers=[], n1=11, n2=null, nOthers={\"n3\": 33})");
+        assertOutput("<~a 1 n1=11 a=1 b=2 c=3 d=4 e=5 f=6 g=7 />",
+                "#a(p1=1, p2=null, pOthers=[], n1=11, n2=null, nOthers={"
+                        + "\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4, \"e\": 5, \"f\": 6, \"g\": 7})");
+
+        assertOutput("<~a; a, b, c/>",
+                "#a(p1=null, p2=null, pOthers=[], n1=null, n2=null, nOthers={}; 3)");
+        assertOutput("<~a 1, 2; a, b, c />",
+                "#a(p1=1, p2=2, pOthers=[], n1=null, n2=null, nOthers={}; 3)");
+        assertOutput("<~a n1=11 n2=22; a, b, c />",
+                "#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)");
+
+        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={}) {...}");
+    }
+
+    @Test
+    public void testSyntaxEdgeCases() throws IOException, TemplateException {
+        assertOutput("<~a; x/>",
+                "#a(p1=null, p2=null, pOthers=[], n1=null, n2=null, nOthers={}; 1)");
+        assertOutput("<~a;x/>",
+                "#a(p1=null, p2=null, pOthers=[], n1=null, n2=null, nOthers={}; 1)");
+        assertOutput("<~a;x />",
+                "#a(p1=null, p2=null, pOthers=[], n1=null, n2=null, nOthers={}; 1)");
+
+        assertOutput("<~a  1  ,  2 n1 = 11  n2 = 22  ;  a  ,  b  ,  c  />",
+                "#a(p1=1, p2=2, 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)");
+        assertOutput("<~a\t1,2\tn1=11\tn2=22;a,b,c/>",
+                "#a(p1=1, p2=2, pOthers=[], n1=11, n2=22, nOthers={}; 3)");
+
+        assertOutput("<~a + 1 />",
+                "#a(p1=1, p2=null, pOthers=[], n1=null, n2=null, nOthers={})");
+    }
+
+    @Test
+    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");
+        assertErrorContains("<~a n1=1, 2 />", "Remove comma", "between", "by position");
+        assertErrorContains("<~a, 1 />", "Remove comma", "between", "by position");
+        assertErrorContains("<~a 1, , 2 />", "Two commas");
+        assertErrorContains("<~a 1 2 />", "Missing comma");
+        assertErrorContains("<~a n1=1 2 />", "must be earlier than arguments passed by name");
+    }
+
+    @Test
+    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");
+        assertErrorContains("<~n n3=9 />", "has no", "\"n3\"", "supported", "\"n1\", \"n2\"");
+        assertErrorContains("<~p n1=9 />", "doesn't have any by-name-passed");
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/org/apache/freemarker/core/model/ConstantsTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/org/apache/freemarker/core/model/ConstantsTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/org/apache/freemarker/core/model/ConstantsTest.java
new file mode 100644
index 0000000..0817bf9
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/org/apache/freemarker/core/model/ConstantsTest.java
@@ -0,0 +1,21 @@
+package org.apache.freemarker.core.model.impl.org.apache.freemarker.core.model;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.Constants;
+import org.apache.freemarker.test.TemplateTest;
+import org.junit.Test;
+
+public final class ConstantsTest extends TemplateTest {
+
+    @Test
+    public void testEmptyHash() throws IOException, TemplateException {
+        addToDataModel("h", Constants.EMPTY_HASH);
+        assertOutput("{<#list h as k ,v>x</#list>}", "{}");
+        assertOutput("{<#list h?keys as k>x</#list>}", "{}");
+        assertOutput("{<#list h?values as k>x</#list>}", "{}");
+        assertOutput("${h?size}", "0");
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/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
new file mode 100644
index 0000000..b7baf56
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AllFeaturesDirective.java
@@ -0,0 +1,133 @@
+/*
+ * 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.userpkg;
+
+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;
+import org.apache.freemarker.core.model.CallPlace;
+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;
+
+public class AllFeaturesDirective extends TestTemplateDirectiveModel {
+
+    private static final int P1_ARG_IDX = 0;
+    private static final int P2_ARG_IDX = 1;
+    private static final int P_OTHERS_ARG_IDX = 2;
+    private static final int N1_ARG_IDX = 3;
+    private static final int N2_ARG_IDX = 4;
+    private static final int N_OTHERS_IDX = 5;
+
+    private static final String N1_ARG_NAME = "n1";
+    private static final String N2_ARG_NAME = "n2";
+
+    private final boolean p1AllowNull;
+    private final boolean p2AllowNull;
+    private final boolean n1AllowNull;
+    private final boolean n2AllowNull;
+
+    public AllFeaturesDirective() {
+        this(true, true, true, true);
+    }
+
+    public AllFeaturesDirective(boolean p1AllowNull, boolean p2AllowNull, boolean n1AllowNull, boolean n2AllowNull) {
+        this.p1AllowNull = p1AllowNull;
+        this.p2AllowNull = p2AllowNull;
+        this.n1AllowNull = n1AllowNull;
+        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();
+
+    @Override
+    public void execute(TemplateModel[] args, Writer out, Environment env, CallPlace callPlace)
+            throws TemplateException, IOException {
+        execute(castArgumentToNumber(args, P1_ARG_IDX, p1AllowNull, env),
+                castArgumentToNumber(args, P2_ARG_IDX, p2AllowNull, env),
+                (TemplateSequenceModel) args[P_OTHERS_ARG_IDX],
+                castArgumentToNumber(args[N1_ARG_IDX], N1_ARG_NAME, n1AllowNull, env),
+                castArgumentToNumber(args[N2_ARG_IDX], N2_ARG_NAME, n2AllowNull, env),
+                (TemplateHashModelEx2) args[N_OTHERS_IDX],
+                out, env, callPlace);
+    }
+
+    private void execute(TemplateNumberModel p1, TemplateNumberModel p2, TemplateSequenceModel pOthers,
+            TemplateNumberModel n1, TemplateNumberModel n2, TemplateHashModelEx2 nOthers,
+            Writer out, Environment env, CallPlace callPlace) throws IOException, TemplateException {
+        out.write("#a(");
+        printParam("p1", p1, out, true);
+        printParam("p2", p2, out);
+        printParam("pOthers", pOthers, out);
+        printParam(N1_ARG_NAME, n1, out);
+        printParam(N2_ARG_NAME, n2, out);
+        printParam("nOthers", nOthers, out);
+        if (callPlace.getLoopVariableCount() != 0) {
+            out.write("; " + callPlace.getLoopVariableCount());
+        }
+        out.write(")");
+        if (callPlace.hasNestedContent()) {
+            out.write(" {...}");
+        }
+    }
+
+    @Override
+    public int getPredefinedPositionalArgumentCount() {
+        return 2;
+    }
+
+    @Override
+    public boolean hasPositionalVarargsArgument() {
+        return true;
+    }
+
+    @Override
+    public int getNamedArgumentIndex(String name) {
+        Integer idx = PARAM_NAME_TO_IDX.get(name);
+        return idx != null ? idx : -1;
+    }
+
+    @Override
+    public int getNamedVarargsArgumentIndex() {
+        return N_OTHERS_IDX;
+    }
+
+    @Override
+    public Collection<String> getPredefinedNamedArgumentNames() {
+        return PARAM_NAME_TO_IDX.keySet();
+    }
+
+    @Override
+    public int getTotalArgumentCount() {
+        return N_OTHERS_IDX + 1;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TestTemplateDirectiveModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TestTemplateDirectiveModel.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TestTemplateDirectiveModel.java
new file mode 100644
index 0000000..c69497a
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TestTemplateDirectiveModel.java
@@ -0,0 +1,88 @@
+/*
+ * 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.userpkg;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.freemarker.core.model.TemplateDirectiveModel2;
+import org.apache.freemarker.core.model.TemplateHashModelEx2;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.util.FTLUtil;
+
+public abstract class TestTemplateDirectiveModel implements TemplateDirectiveModel2 {
+
+    protected void printParam(String name, TemplateModel value, Writer out) throws IOException, TemplateModelException {
+        printParam(name, value, out, false);
+    }
+
+    protected void printParam(String name, TemplateModel value, Writer out, boolean first)
+            throws IOException, TemplateModelException {
+        if (!first) {
+            out.write(", ");
+        }
+        out.write(name);
+        out.write("=");
+        printValue(value, out);
+    }
+
+    private void printValue(TemplateModel value, Writer out) throws IOException, TemplateModelException {
+        if (value == null) {
+            out.write("null");
+        } else if (value instanceof TemplateNumberModel) {
+            out.write(((TemplateNumberModel) value).getAsNumber().toString());
+        } else if (value instanceof TemplateScalarModel) {
+            out.write(FTLUtil.toStringLiteral(((TemplateScalarModel) value).getAsString()));
+        } else if (value instanceof TemplateSequenceModel) {
+            int len = ((TemplateSequenceModel) value).size();
+            out.write('[');
+            for (int i = 0; i < len; i++) {
+                if (i != 0) {
+                    out.write(", ");
+                }
+                printValue(((TemplateSequenceModel) value).get(i), out);
+            }
+            out.write(']');
+        } else if (value instanceof TemplateHashModelEx2) {
+            TemplateHashModelEx2.KeyValuePairIterator it = ((TemplateHashModelEx2) value).keyValuePairIterator();
+            out.write('{');
+            while (it.hasNext()) {
+                TemplateHashModelEx2.KeyValuePair kvp = it.next();
+
+                printValue(kvp.getKey(), out);
+                out.write(": ");
+                printValue(kvp.getValue(), out);
+
+                if (it.hasNext()) {
+                    out.write(", ");
+                }
+            }
+            out.write('}');
+        } else {
+            throw new IllegalArgumentException("Unsupported value class: " + value.getClass().getName());
+        }
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/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
new file mode 100644
index 0000000..4d2d635
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoNamedParamsDirective.java
@@ -0,0 +1,85 @@
+/*
+ * 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.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;
+
+public class TwoNamedParamsDirective extends TestTemplateDirectiveModel {
+
+    private static final String N1_ARG_NAME = "n1";
+    private static final String N2_ARG_NAME = "n2";
+    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();
+
+    @Override
+    public void execute(TemplateModel[] args, Writer out, Environment env, CallPlace callPlace)
+            throws TemplateException, IOException {
+        out.write("#n(");
+        printParam(N1_ARG_NAME, args[N1_ARG_IDX], out, true);
+        printParam(N2_ARG_NAME, args[N2_ARG_IDX], out);
+        out.write(")");
+    }
+
+    @Override
+    public int getPredefinedPositionalArgumentCount() {
+        return 0;
+    }
+
+    @Override
+    public boolean hasPositionalVarargsArgument() {
+        return false;
+    }
+
+    @Override
+    public int getNamedArgumentIndex(String name) {
+        Integer idx = PARAM_NAME_TO_IDX.get(name);
+        return idx != null ? idx : -1;
+    }
+
+    @Override
+    public int getNamedVarargsArgumentIndex() {
+        return -1;
+    }
+
+    @Override
+    public int getTotalArgumentCount() {
+        return PARAM_NAME_TO_IDX.size();
+    }
+
+    @Override
+    public Collection<String> getPredefinedNamedArgumentNames() {
+        return PARAM_NAME_TO_IDX.keySet();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoPositionalParamsDirective.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoPositionalParamsDirective.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoPositionalParamsDirective.java
new file mode 100644
index 0000000..e9eb926
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoPositionalParamsDirective.java
@@ -0,0 +1,71 @@
+/*
+ * 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.userpkg;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+
+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;
+
+public class TwoPositionalParamsDirective extends TestTemplateDirectiveModel {
+
+    @Override
+    public void execute(TemplateModel[] args, Writer out, Environment env, CallPlace callPlace)
+            throws TemplateException, IOException {
+        out.write("#p(");
+        printParam("p1", args[0], out, true);
+        printParam("p2", args[1], out);
+        out.write(")");
+    }
+
+    @Override
+    public int getPredefinedPositionalArgumentCount() {
+        return 2;
+    }
+
+    @Override
+    public boolean hasPositionalVarargsArgument() {
+        return false;
+    }
+
+    @Override
+    public int getNamedArgumentIndex(String name) {
+        return -1;
+    }
+
+    @Override
+    public int getNamedVarargsArgumentIndex() {
+        return -1;
+    }
+
+    @Override
+    public int getTotalArgumentCount() {
+        return getPredefinedPositionalArgumentCount();
+    }
+
+    @Override
+    public Collection<String> getPredefinedNamedArgumentNames() {
+        return null;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/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
new file mode 100644
index 0000000..52ae027
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirDynamicDirectiveCall.java
@@ -0,0 +1,422 @@
+/*
+ * 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/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/NonNumericalException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonNumericalException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonNumericalException.java
index f70bd83..1b9b586 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/NonNumericalException.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonNumericalException.java
@@ -19,6 +19,8 @@
 
 package org.apache.freemarker.core;
 
+import java.io.Serializable;
+
 import org.apache.freemarker.core.model.TemplateModel;
 import org.apache.freemarker.core.model.TemplateNumberModel;
 
@@ -28,7 +30,7 @@ import org.apache.freemarker.core.model.TemplateNumberModel;
 public class NonNumericalException extends UnexpectedTypeException {
 
     private static final Class[] EXPECTED_TYPES = new Class[] { TemplateNumberModel.class };
-    
+
     public NonNumericalException(Environment env) {
         super(env, "Expecting numerical value here");
     }
@@ -64,6 +66,14 @@ public class NonNumericalException extends UnexpectedTypeException {
             throws InvalidReferenceException {
         super(assignmentTargetVarName, model, "number", EXPECTED_TYPES, tips, env);
     }
+
+
+    NonNumericalException(
+            Serializable argumentNameOrIndex, TemplateModel model, String[] tips, Environment env)
+            throws InvalidReferenceException {
+        super(argumentNameOrIndex, model, "number", EXPECTED_TYPES, tips, env);
+    }
+
     static NonNumericalException newMalformedNumberException(ASTExpression blamed, String text, Environment env) {
         return new NonNumericalException(
                 new _ErrorDescriptionBuilder("Can't convert this string to number: ", new _DelayedJQuote(text))

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java
index 4cf353b..1daae3e 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java
@@ -23,6 +23,7 @@ import org.apache.freemarker.core.model.TemplateDirectiveModel;
 import org.apache.freemarker.core.model.TemplateModel;
 import org.apache.freemarker.core.model.TemplateTransformModel;
 
+// TODO [FM3][CF] Review and rename this when TDM2 and TFM are in place
 /**
  * Indicates that a {@link TemplateDirectiveModel} or {@link TemplateTransformModel} or {@link ASTDirMacro} value was
  * expected, but the value had a different type.

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateCallableModelUtils.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateCallableModelUtils.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateCallableModelUtils.java
new file mode 100644
index 0000000..4ee9f77
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateCallableModelUtils.java
@@ -0,0 +1,48 @@
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+
+public class TemplateCallableModelUtils {
+
+    public static final TemplateModel[] EMPTY_TEMPLATE_MODEL_ARRAY = new TemplateModel[0];
+
+    // TODO [FM3][CF] Add this to the other exception classes too
+    public static TemplateNumberModel castArgumentToNumber(TemplateModel[] args, int argIndex, boolean allowNull,
+            Environment env) throws TemplateException {
+        return getTemplateNumberModel(args[argIndex], argIndex, allowNull, env);
+    }
+
+    // TODO [FM3][CF] Add this to the other exception classes too
+    private static TemplateNumberModel getTemplateNumberModel(TemplateModel argValue, int argIndex, boolean allowNull,
+            Environment env) throws TemplateException {
+        if (argValue instanceof TemplateNumberModel) {
+            return (TemplateNumberModel) argValue;
+        }
+        if (argValue == null) {
+            if (allowNull) {
+                return null;
+            }
+            throw new _MiscTemplateException(env,
+                    "The ", new _DelayedOrdinal(argIndex + 1), " argument can't be null.");
+        }
+        throw new NonNumericalException(argIndex, argValue, null, env);
+    }
+
+    // TODO [FM3][CF] Add this to the other exception classes too
+    public static TemplateNumberModel castArgumentToNumber(TemplateModel argValue, String argName, boolean allowNull,
+            Environment env) throws TemplateException {
+        if (argValue instanceof TemplateNumberModel) {
+            return (TemplateNumberModel) argValue;
+        }
+        if (argValue == null) {
+            if (allowNull) {
+                return null;
+            }
+            throw new _MiscTemplateException(env,
+                    "The ", new _DelayedJQuote(argName), " argument can't be null.");
+        }
+        throw new NonNumericalException(argName, argValue, null, env);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/UnexpectedTypeException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/UnexpectedTypeException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/UnexpectedTypeException.java
index 939d8d2..741eed0 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/UnexpectedTypeException.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/UnexpectedTypeException.java
@@ -19,6 +19,8 @@
 
 package org.apache.freemarker.core;
 
+import java.io.Serializable;
+
 import org.apache.freemarker.core.model.TemplateModel;
 
 /**
@@ -37,14 +39,16 @@ public class UnexpectedTypeException extends TemplateException {
     UnexpectedTypeException(
             ASTExpression blamed, TemplateModel model, String expectedTypesDesc, Class[] expectedTypes, Environment env)
             throws InvalidReferenceException {
-        super(null, env, blamed, newDesciptionBuilder(blamed, null, model, expectedTypesDesc, expectedTypes, env));
+        super(null, env, blamed, newDescriptionBuilder(blamed, null, null, model, expectedTypesDesc, expectedTypes,
+                env));
     }
 
     UnexpectedTypeException(
             ASTExpression blamed, TemplateModel model, String expectedTypesDesc, Class[] expectedTypes, String tip,
             Environment env)
             throws InvalidReferenceException {
-        super(null, env, blamed, newDesciptionBuilder(blamed, null, model, expectedTypesDesc, expectedTypes, env)
+        super(null, env, blamed, newDescriptionBuilder(blamed, null, null, model, expectedTypesDesc, expectedTypes,
+                env)
                 .tip(tip));
     }
 
@@ -52,32 +56,47 @@ public class UnexpectedTypeException extends TemplateException {
             ASTExpression blamed, TemplateModel model, String expectedTypesDesc, Class[] expectedTypes, Object[] tips,
             Environment env)
             throws InvalidReferenceException {
-        super(null, env, blamed, newDesciptionBuilder(blamed, null, model, expectedTypesDesc, expectedTypes, env)
+        super(null, env, blamed, newDescriptionBuilder(blamed, null, null, model, expectedTypesDesc, expectedTypes, env)
                 .tips(tips));
     }
 
+     /**
+      * Used for assignments that use {@code +=} and such.
+      */
     UnexpectedTypeException(
             String blamedAssignmentTargetVarName, TemplateModel model, String expectedTypesDesc, Class[] expectedTypes,
             Object[] tips,
             Environment env)
             throws InvalidReferenceException {
-        super(null, env, null, newDesciptionBuilder(
-                null, blamedAssignmentTargetVarName, model, expectedTypesDesc, expectedTypes, env).tips(tips));
+        super(null, env, null, newDescriptionBuilder(
+                null, blamedAssignmentTargetVarName, null, model, expectedTypesDesc, expectedTypes, env).tips(tips));
     }
-    
+
     /**
-     * @param blamedAssignmentTargetVarName
-     *            Used for assignments that use {@code +=} and such, in which case the {@code blamed} expression
-     *            parameter will be null {@code null} and this parameter will be non-{null}.
+     * Used when the value of a directive/function argument has a different type than that the directive/function
+     * expects.
      */
-    private static _ErrorDescriptionBuilder newDesciptionBuilder(
-            ASTExpression blamed, String blamedAssignmentTargetVarName,
+    UnexpectedTypeException(
+            Serializable blamedArgumentNameOrIndex, TemplateModel model, String expectedTypesDesc, Class[] expectedTypes,
+            Object[] tips,
+            Environment env)
+            throws InvalidReferenceException {
+        super(null, env, null, newDescriptionBuilder(
+                null, null, blamedArgumentNameOrIndex, model, expectedTypesDesc, expectedTypes, env).tips(tips));
+    }
+
+    private static _ErrorDescriptionBuilder newDescriptionBuilder(
+            ASTExpression blamed, String blamedAssignmentTargetVarName, Serializable blamedArgumentNameOrIndex,
             TemplateModel model, String expectedTypesDesc, Class[] expectedTypes, Environment env)
             throws InvalidReferenceException {
-        if (model == null) throw InvalidReferenceException.getInstance(blamed, env);
+        if (model == null) {
+            throw InvalidReferenceException.getInstance(blamed, env);
+        }
 
         _ErrorDescriptionBuilder errorDescBuilder = new _ErrorDescriptionBuilder(
-                unexpectedTypeErrorDescription(expectedTypesDesc, blamed, blamedAssignmentTargetVarName, model))
+                unexpectedTypeErrorDescription(expectedTypesDesc,
+                        blamed, blamedAssignmentTargetVarName, blamedArgumentNameOrIndex,
+                        model))
                 .blame(blamed).showBlamer(true);
         if (model instanceof _UnexpectedTypeErrorExplainerTemplateModel) {
             Object[] tip = ((_UnexpectedTypeErrorExplainerTemplateModel) model).explainTypeError(expectedTypes);
@@ -90,15 +109,25 @@ public class UnexpectedTypeException extends TemplateException {
 
     private static Object[] unexpectedTypeErrorDescription(
             String expectedTypesDesc,
-            ASTExpression blamed, String blamedAssignmentTargetVarName,
+            ASTExpression blamed, String blamedAssignmentTargetVarName, Serializable blamedArgumentNameOrIndex,
             TemplateModel model) {
         return new Object[] {
-                "Expected ", new _DelayedAOrAn(expectedTypesDesc), ", but ",
-                (blamedAssignmentTargetVarName == null
-                        ? blamed != null ? "this" : "the expression"
-                        : new Object[] {
-                                "assignment target variable ",
-                                new _DelayedJQuote(blamedAssignmentTargetVarName) }), 
+                "Expected ", new _DelayedAOrAn(expectedTypesDesc), ", but ", (
+                        blamedAssignmentTargetVarName != null
+                                ? new Object[] {
+                                        "assignment target variable ",
+                                        new _DelayedJQuote(blamedAssignmentTargetVarName) }
+                        : blamedArgumentNameOrIndex != null
+                                ? new Object[] {
+                                        "the ",
+                                        (blamedArgumentNameOrIndex instanceof Integer
+                                                ? new _DelayedOrdinal(((Integer) blamedArgumentNameOrIndex) + 1)
+                                                : new _DelayedJQuote(blamedArgumentNameOrIndex)),
+                                        " argument"}
+                        : blamed != null
+                                ? "this"
+                        : "the expression"
+                ),
                 " has evaluated to ",
                 new _DelayedAOrAn(new _DelayedFTLTypeDescription(model)),
                 (blamedAssignmentTargetVarName == null ? ":" : ".")};

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedJQuotedListing.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedJQuotedListing.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedJQuotedListing.java
new file mode 100644
index 0000000..e809a92
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedJQuotedListing.java
@@ -0,0 +1,46 @@
+/*
+ * 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.util.Collection;
+
+import org.apache.freemarker.core.util._StringUtil;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _DelayedJQuotedListing extends _DelayedConversionToString {
+
+    public _DelayedJQuotedListing(Collection<?> object) {
+        super(object);
+    }
+
+    @Override
+    protected String doConversion(Object obj) {
+        StringBuilder sb = new StringBuilder();
+        for (Object element : (Collection<?>) obj) {
+            if (sb.length() != 0) {
+                sb.append(", ");
+            }
+            sb.append(_StringUtil.jQuote(element));
+        }
+
+        return sb.toString();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/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
new file mode 100644
index 0000000..1f131a3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/CallPlace.java
@@ -0,0 +1,173 @@
+/*
+ * 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.model;
+
+import java.util.IdentityHashMap;
+
+import org.apache.freemarker.core.CallPlaceCustomDataInitializationException;
+import org.apache.freemarker.core.DirectiveCallPlace;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateCallableModelUtils;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.util.CommonSupplier;
+
+/**
+ * The place in a template from where a directive (like a macro) or function (like a method) is called;
+ * <b>Do not implement this interface yourself</b>, as new methods may be added any time! Only FreeMarker itself
+ * should provide implementations.
+ */
+// TODO [FM3][CF] Should also replace DirectiveCallPlace
+public interface CallPlace {
+
+    // -------------------------------------------------------------------------------------------------------------
+    // Nested content:
+
+    /**
+     * Tells if there's a non-zero-length nested content. This is {@code false} for {@code <@foo />} or
+     * {@code <@f...@foo>} or for calls inside expressions (i.e., for function calls).
+     */
+    boolean hasNestedContent();
+
+    /**
+     * The number of loop variables specified for this call.
+     */
+    int getLoopVariableCount();
+
+    /**
+     * Executed the nested content; it there's none, it just does nothing.
+     *
+     * @param loopVariableValues
+     *         The loop variables to pass to the nested content; not {@code null} (use {@link
+     *         TemplateCallableModelUtils#EMPTY_TEMPLATE_MODEL_ARRAY}. Its length must be equal to
+     *         {@link #getLoopVariableCount()}.
+     */
+    void executeNestedContent(TemplateModel[] loopVariableValues, Environment env) throws TemplateException;
+
+    // -------------------------------------------------------------------------------------------------------------
+    // Source code info:
+
+    /**
+     * The template that contains this call.
+     */
+    Template getTemplate();
+
+    /**
+     * The 1-based column number of the first character of the directive call in the template source code, or -1 if it's
+     * not known.
+     */
+    int getBeginColumn();
+
+    /**
+     * The 1-based line number of the first character of the directive call in the template source code, or -1 if it's
+     * not known.
+     */
+    int getBeginLine();
+
+    /**
+     * The 1-based column number of the last character of the directive call in the template source code, or -1 if it's
+     * not known. If the directive has an end-tag ({@code </...@...>}), then it points to the last character of that.
+     */
+    int getEndColumn();
+
+    /**
+     * The 1-based line number of the last character of the directive call in the template source code, or -1 if it's
+     * not known. If the directive has an end-tag ({@code </...@...>}), then it points to the last character of that.
+     */
+    int getEndLine();
+
+    // -------------------------------------------------------------------------------------------------------------
+    // Caching:
+
+    /**
+     * Returns the custom data, or if that's {@code null}, then it creates and stores it in an atomic operation then
+     * returns it. This method is thread-safe, however, it doesn't ensure thread safe (like synchronized) access to the
+     * custom data itself. See the top-level documentation of {@link DirectiveCallPlace} to understand the scope and
+     * life-cycle of the custom data. Be sure that the custom data only depends on things that get their final value
+     * during template parsing, not on runtime settings.
+     *
+     * <p>
+     * This method will block other calls while the {@code supplier} is executing, thus, the object will be
+     * <em>usually</em> created only once, even if multiple threads request the value when it's still {@code null}. It
+     * doesn't stand though when {@code providerIdentity} mismatches occur (see later). Furthermore, then it's also
+     * possible that multiple objects created by the same {@link CommonSupplier} will be in use on the same time, because
+     * of directive executions already running in parallel, and because of memory synchronization delays (hardware
+     * dependent) between the threads.
+     *
+     * @param providerIdentity
+     *            This is usually the class of the {@link TemplateDirectiveModel} that creates (and uses) the custom
+     *            data, or if you are using your own class for the custom data object (as opposed to a class from some
+     *            more generic API), then that class. This is needed as the same call place might calls different
+     *            directives depending on runtime conditions, and so it must be ensured that these directives won't
+     *            accidentally read each other's custom data, ending up with class cast exceptions or worse. In the
+     *            current implementation, if there's a {@code providerIdentity} mismatch (means, the
+     *            {@code providerIdentity} object used when the custom data was last set isn't the exactly same object
+     *            as the one provided with the parameter now), the previous custom data will be just ignored as if it
+     *            was {@code null}. So if multiple directives that use the custom data feature use the same call place,
+     *            the caching of the custom data can be inefficient, as they will keep overwriting each other's custom
+     *            data. (In a more generic implementation the {@code providerIdentity} would be a key in a
+     *            {@link IdentityHashMap}, but then this feature would be slower, while {@code providerIdentity}
+     *            mismatches aren't occurring in most applications.)
+     * @param supplier
+     *            Called when the custom data wasn't yet set, to invoke its initial value. If this parameter is
+     *            {@code null} and the custom data wasn't set yet, then {@code null} will be returned. The returned
+     *            value of {@link CommonSupplier#get()} can be any kind of object, but can't be {@code null}.
+     *
+     * @return The current custom data object, or possibly {@code null} if there was no {@link CommonSupplier} provided.
+     *
+     * @throws CallPlaceCustomDataInitializationException
+     *             If the {@link CommonSupplier} had to be invoked but failed.
+     */
+    Object getOrCreateCustomData(Object providerIdentity, CommonSupplier supplier)
+            throws CallPlaceCustomDataInitializationException;
+
+    /**
+     * Tells if the nested content (the body) can be safely cached, as it only depends on the template content (not on
+     * variable values and such) and has no side-effects (other than writing to the output). Examples of cases that give
+     * {@code false}: {@code <@foo>Name: } <tt...@foo>},
+     * {@code <@foo>Name: <#if showIt>Joe</#...@foo>}. Examples of cases that give {@code true}:
+     * {@code <@foo>Name: Joe</...@foo>}, {@code <@foo />}. Note that we get {@code true} for no nested content, because
+     * that's equivalent to 0-length nested content.
+     * <p>
+     * This method returns a pessimistic result. For example, if it sees a custom directive call, it can't know what it
+     * does, so it will assume that it's not cacheable.
+     */
+    boolean isNestedOutputCacheable();
+
+    // -------------------------------------------------------------------------------------------------------------
+    // Miscellaneous:
+
+    /**
+     * Used solely for speed optimization (to minimize the number of
+     * {@link #getTargetJavaParameterType(int)} calls).
+     *
+     * @return -1 if no parameter has type hint
+     */
+    int getFirstTargetJavaParameterTypeIndex();
+
+    /**
+     * The type of the parameter in the target Java method; used for overloaded Java method selection.
+     * This optional information is specified by the template author in the source code.
+     *
+     * @return The desired type or {@code null} if this information wasn't specified in the template.
+     */
+    Class<?> getTargetJavaParameterType(int argIndex);
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/model/Constants.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/Constants.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/Constants.java
index 268188d..f72d3e6 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/model/Constants.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/Constants.java
@@ -20,7 +20,9 @@
 package org.apache.freemarker.core.model;
 
 import java.io.Serializable;
+import java.util.NoSuchElementException;
 
+import org.apache.freemarker.core.model.TemplateHashModelEx2.KeyValuePairIterator;
 import org.apache.freemarker.core.model.impl.SimpleNumber;
 
 /**
@@ -47,7 +49,7 @@ public class Constants {
     public static final TemplateNumberModel MINUS_ONE = new SimpleNumber(-1);
     
     public static final TemplateModelIterator EMPTY_ITERATOR = new EmptyIteratorModel();
-    
+
     private static class EmptyIteratorModel implements TemplateModelIterator, Serializable {
 
         @Override
@@ -101,7 +103,7 @@ public class Constants {
     
     public static final TemplateHashModelEx EMPTY_HASH = new EmptyHashModel();
     
-    private static class EmptyHashModel implements TemplateHashModelEx, Serializable {
+    private static class EmptyHashModel implements TemplateHashModelEx2, Serializable {
         
         @Override
         public int size() throws TemplateModelException {
@@ -127,7 +129,33 @@ public class Constants {
         public boolean isEmpty() throws TemplateModelException {
             return true;
         }
-        
+
+        @Override
+        public KeyValuePairIterator keyValuePairIterator() throws TemplateModelException {
+            return EMPTY_KEY_VALUE_PAIR_ITERATOR;
+        }
     }
-    
+
+    public static final KeyValuePairIterator EMPTY_KEY_VALUE_PAIR_ITERATOR = EmptyKeyValuePairIterator.INSTANCE;
+
+    private static class EmptyKeyValuePairIterator implements TemplateHashModelEx2.KeyValuePairIterator {
+
+        static final EmptyKeyValuePairIterator INSTANCE = new EmptyKeyValuePairIterator();
+
+        private EmptyKeyValuePairIterator() {
+            //
+        }
+
+        @Override
+        public boolean hasNext() throws TemplateModelException {
+            return false;
+        }
+
+        @Override
+        public TemplateHashModelEx2.KeyValuePair next() throws TemplateModelException {
+            throw new NoSuchElementException("Can't retrieve element from empty key-value pair iterator.");
+        }
+
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/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
new file mode 100644
index 0000000..bc053bd
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCallableModel.java
@@ -0,0 +1,86 @@
+/*
+ * 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.model;
+
+import java.util.Collection;
+
+/**
+ * Super interface of {@link TemplateFunctionModel} and {@link TemplateDirectiveModel2}.
+ */
+public interface TemplateCallableModel extends TemplateModel {
+
+    // -------------------------------------------------------------------------------------------------------------
+    // Arguments:
+
+    /**
+     * The highest allowed number of positional arguments, not counting the positional varargs argument. The actual
+     * positional argument count can be less than this if there are optional positional argument. When calling the
+     * {@code execute} method, this many items will be reserved for the positional arguments in the argument array (not
+     * counting the item for the positional varargs argument, if there's one). Positional arguments
+     * above this count will go to the varargs argument (if there's one, otherwise it's an error).
+     */
+    int getPredefinedPositionalArgumentCount();
+
+    /**
+     * Tells if there's no position varargs argument. If there is, then it must be in the argument array at the
+     * index equals to {@link #getPredefinedPositionalArgumentCount()}. The positional varargs argument is a
+     * {@link TemplateSequenceModel} that collects all positional arguments whose index would be greater than
+     * or equal to {@link #getPredefinedPositionalArgumentCount()}.
+     */
+    boolean hasPositionalVarargsArgument();
+
+    /**
+     * Returns if with what array index should the given named argument by passed to the {@code execute} method.
+     *
+     * @return -1 if there's no such named argument
+     */
+    int getNamedArgumentIndex(String name);
+
+    /**
+     * Returns the index of the named varargs argument in the argument array, or -1 if there's no named varargs
+     * argument. The named varargs argument is a {@link TemplateHashModelEx2} with string keys that collects all
+     * the named arguments for which {@link #getNamedArgumentIndex(String)} returns -1. The iteration order of this
+     * hash follows the order in which the arguments were specified in the calling template.
+     *
+     * @return -1 if there's no named varargs argument
+     */
+    int getNamedVarargsArgumentIndex();
+
+    /**
+     * The required length of the arguments array passed to the {@code execute} method. (It's possible that a longer
+     * array will be passed, in which case the extra elements should be ignored by {@code execute}.)
+     * The return value should be equal to the sum of these (but we don't want to calculate it on-the-fly, for speed),
+     * or else FreeMarker might fails later with {@link IndexOutOfBoundsException}:
+     * <ul>
+     *     <li>{@link #getPredefinedPositionalArgumentCount()}
+     *     <li>If {@link #hasPositionalVarargsArgument()} is {@code true}, then 1, else 0.
+     *     <li>Size of {@link #getPredefinedNamedArgumentNames()}
+     *     <li>If {@link #getNamedVarargsArgumentIndex()} is not -1, then 1, else 0. (Also, obviously, if
+     *     {@link #getNamedVarargsArgumentIndex()} is not -1, then it's one less than the return value of this method.)
+     * </ul>
+     */
+    int getTotalArgumentCount();
+
+    /**
+     * The valid named argument names in the order as they should be displayed in error messages, or {@code null}.
+     */
+    Collection<String> getPredefinedNamedArgumentNames();
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDirectiveModel2.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDirectiveModel2.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDirectiveModel2.java
new file mode 100644
index 0000000..e430639
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDirectiveModel2.java
@@ -0,0 +1,28 @@
+package org.apache.freemarker.core.model;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.TemplateException;
+
+/**
+ * A {@link TemplateCallableModel} that (progressively) prints it result into the {@code out} object, instead of
+ * returning a single result at the end of the execution. Many of these won't print anything, but has other
+ * side-effects why it's useful for calling them. When used in an expression context, the printer output will be the
+ * value of the call (which depending on the output format of the directive is a {@link TemplateMarkupOutputModel},
+ * or a {@link String}).
+ */
+// TODO [FM3][CF] Rename this to TemplateDirectiveModel
+public interface TemplateDirectiveModel2 extends TemplateCallableModel {
+
+    /**
+     * @param args Array with {@link #getTotalArgumentCount()} elements (or more, in which case the extra elements
+     *             should be ignored). If a parameter was omitted, the corresponding array element will be {@code null}.
+     */
+    void execute(
+            TemplateModel[] args, Writer out,
+            Environment env, CallPlace callPlace)
+            throws TemplateException, IOException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateFunctionModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateFunctionModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateFunctionModel.java
new file mode 100644
index 0000000..7944d81
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateFunctionModel.java
@@ -0,0 +1,25 @@
+package org.apache.freemarker.core.model;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.ProcessingConfiguration;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+
+/**
+ * A {@link TemplateCallableModel}, which returns its result as a {@link TemplateModel} at the end of the execution.
+ * This is in contrast with {@link TemplateDirectiveModel2}, which writes its result progressively to the output.
+ *
+ * <p>Some template languages may allow function calls directly embedded into static text, as in
+ * <code>text#f()text</code>. In that case, the language has to ensure that the return value is formatted according
+ * the {@link ProcessingConfiguration} settings (such as {@link ProcessingConfiguration#getNumberFormat()} and
+ * {@link ProcessingConfiguration#getDateFormat()}), and is printed to the output after escaping according the
+ * {@link OutputFormat} of the context. Some template languages instead require using an explicit expression value
+ * printer statement, as in <code>text${f()}text</code>.
+ */
+public interface TemplateFunctionModel extends TemplateCallableModel {
+
+    TemplateModel execute(
+            TemplateModel[] args, Environment env, CallPlace callPlace)
+            throws TemplateException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java
index 0752f1b..1da4f62 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java
@@ -49,6 +49,7 @@ import org.apache.freemarker.core.model.impl.BeanModel;
  * Static utility methods that perform tasks specific to the FreeMarker Template Language (FTL).
  * This is meant to be used from outside FreeMarker (i.e., it's an official, published API), not just from inside it.
  */
+// TODO [FM3] This name won't be good if the template language isn't called "FTL"...
 public final class FTLUtil {
 
     private static final char[] ESCAPES = createEscapes();