http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpHashLiteral.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpHashLiteral.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpHashLiteral.java
new file mode 100644
index 0000000..792787a
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpHashLiteral.java
@@ -0,0 +1,220 @@
+/*
+ * 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.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.ListIterator;
+
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+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.TemplateModelIterator;
+import org.apache.freemarker.core.model.impl.CollectionAndSequence;
+
+/**
+ * AST expression node: <tt>{ keyExp: valueExp, ... }</tt> 
+ */
+final class ASTExpHashLiteral extends ASTExpression {
+
+    private final ArrayList keys, values;
+    private final int size;
+
+    ASTExpHashLiteral(ArrayList/*<ASTExpression>*/ keys, 
ArrayList/*<ASTExpression>*/ values) {
+        this.keys = keys;
+        this.values = values;
+        size = keys.size();
+        keys.trimToSize();
+        values.trimToSize();
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        return new SequenceHash(env);
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        StringBuilder buf = new StringBuilder("{");
+        for (int i = 0; i < size; i++) {
+            ASTExpression key = (ASTExpression) keys.get(i);
+            ASTExpression value = (ASTExpression) values.get(i);
+            buf.append(key.getCanonicalForm());
+            buf.append(": ");
+            buf.append(value.getCanonicalForm());
+            if (i != size - 1) {
+                buf.append(", ");
+            }
+        }
+        buf.append("}");
+        return buf.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "{...}";
+    }
+
+    @Override
+    boolean isLiteral() {
+        if (constantValue != null) {
+            return true;
+        }
+        for (int i = 0; i < size; i++) {
+            ASTExpression key = (ASTExpression) keys.get(i);
+            ASTExpression value = (ASTExpression) values.get(i);
+            if (!key.isLiteral() || !value.isLiteral()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, 
ReplacemenetState replacementState) {
+               ArrayList clonedKeys = (ArrayList) keys.clone();
+               for (ListIterator iter = clonedKeys.listIterator(); 
iter.hasNext(); ) {
+            iter.set(((ASTExpression) 
iter.next()).deepCloneWithIdentifierReplaced(
+                    replacedIdentifier, replacement, replacementState));
+        }
+               ArrayList clonedValues = (ArrayList) values.clone();
+               for (ListIterator iter = clonedValues.listIterator(); 
iter.hasNext(); ) {
+            iter.set(((ASTExpression) 
iter.next()).deepCloneWithIdentifierReplaced(
+                    replacedIdentifier, replacement, replacementState));
+        }
+       return new ASTExpHashLiteral(clonedKeys, clonedValues);
+    }
+
+    private class SequenceHash implements TemplateHashModelEx2 {
+
+        private HashMap<String, TemplateModel> map;
+        private TemplateCollectionModel keyCollection, valueCollection; // 
ordered lists of keys and values
+
+        SequenceHash(Environment env) throws TemplateException {
+            map = new LinkedHashMap<>();
+            for (int i = 0; i < size; i++) {
+                ASTExpression keyExp = (ASTExpression) keys.get(i);
+                ASTExpression valExp = (ASTExpression) values.get(i);
+                String key = keyExp.evalAndCoerceToPlainText(env);
+                TemplateModel value = valExp.eval(env);
+                valExp.assertNonNull(value, env);
+                map.put(key, value);
+            }
+        }
+
+        @Override
+        public int size() {
+            return size;
+        }
+
+        @Override
+        public TemplateCollectionModel keys() {
+            if (keyCollection == null) {
+                keyCollection = new CollectionAndSequence(new 
NativeStringCollectionCollectionEx(map.keySet()));
+            }
+            return keyCollection;
+        }
+
+        @Override
+        public TemplateCollectionModel values() {
+            if (valueCollection == null) {
+                valueCollection = new CollectionAndSequence(new 
NativeCollectionEx(map.values()));
+            }
+            return valueCollection;
+        }
+
+        @Override
+        public TemplateModel get(String key) {
+            return map.get(key);
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return size == 0;
+        }
+        
+        @Override
+        public String toString() {
+            return getCanonicalForm();
+        }
+
+        @Override
+        public KeyValuePairIterator keyValuePairIterator() throws 
TemplateModelException {
+            return new KeyValuePairIterator() {
+                private final TemplateModelIterator keyIterator = 
keys().iterator();
+                private final TemplateModelIterator valueIterator = 
values().iterator();
+
+                @Override
+                public boolean hasNext() throws TemplateModelException {
+                    return keyIterator.hasNext();
+                }
+
+                @Override
+                public KeyValuePair next() throws TemplateModelException {
+                    return new KeyValuePair() {
+                        private final TemplateModel key = keyIterator.next();
+                        private final TemplateModel value = 
valueIterator.next();
+
+                        @Override
+                        public TemplateModel getKey() throws 
TemplateModelException {
+                            return key;
+                        }
+
+                        @Override
+                        public TemplateModel getValue() throws 
TemplateModelException {
+                            return value;
+                        }
+                        
+                    };
+                }
+                
+            };
+        }
+        
+    }
+
+    @Override
+    int getParameterCount() {
+        return size * 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        checkIndex(idx);
+        return idx % 2 == 0 ? keys.get(idx / 2) : values.get(idx / 2);
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        checkIndex(idx);
+        return idx % 2 == 0 ? ParameterRole.ITEM_KEY : 
ParameterRole.ITEM_VALUE;
+    }
+
+    private void checkIndex(int idx) {
+        if (idx >= size * 2) {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java
new file mode 100644
index 0000000..b3fba1f
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpListLiteral.java
@@ -0,0 +1,195 @@
+/*
+ * 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.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+
+import org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+
+/**
+ * AST expression node: {@code [ exp, ... ]} 
+ */
+final class ASTExpListLiteral extends ASTExpression {
+
+    final ArrayList/*<ASTExpression>*/ items;
+
+    ASTExpListLiteral(ArrayList items) {
+        this.items = items;
+        items.trimToSize();
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        NativeSequence list = new NativeSequence(items.size());
+        for (Object item : items) {
+            ASTExpression exp = (ASTExpression) item;
+            TemplateModel tm = exp.eval(env);
+            exp.assertNonNull(tm, env);
+            list.add(tm);
+        }
+        return list;
+    }
+
+    /**
+     * For {@link TemplateMethodModel} calls, but not for {@link 
TemplateMethodModelEx}-es, returns the list of
+     * arguments as {@link String}-s.
+     */
+    List/*<String>*/ getValueList(Environment env) throws TemplateException {
+        int size = items.size();
+        switch(size) {
+            case 0: {
+                return Collections.EMPTY_LIST;
+            }
+            case 1: {
+                return Collections.singletonList(((ASTExpression) 
items.get(0)).evalAndCoerceToPlainText(env));
+            }
+            default: {
+                List result = new ArrayList(items.size());
+                for (ListIterator iterator = items.listIterator(); 
iterator.hasNext(); ) {
+                    ASTExpression exp = (ASTExpression) iterator.next();
+                    result.add(exp.evalAndCoerceToPlainText(env));
+                }
+                return result;
+            }
+        }
+    }
+
+    /**
+     * For {@link TemplateMethodModelEx} calls, returns the list of arguments 
as {@link TemplateModel}-s.
+     */
+    List/*<TemplateModel>*/ getModelList(Environment env) throws 
TemplateException {
+        int size = items.size();
+        switch(size) {
+            case 0: {
+                return Collections.EMPTY_LIST;
+            }
+            case 1: {
+                return Collections.singletonList(((ASTExpression) 
items.get(0)).eval(env));
+            }
+            default: {
+                List result = new ArrayList(items.size());
+                for (ListIterator iterator = items.listIterator(); 
iterator.hasNext(); ) {
+                    ASTExpression exp = (ASTExpression) iterator.next();
+                    result.add(exp.eval(env));
+                }
+                return result;
+            }
+        }
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        StringBuilder buf = new StringBuilder("[");
+        int size = items.size();
+        for (int i = 0; i < size; i++) {
+            ASTExpression value = (ASTExpression) items.get(i);
+            buf.append(value.getCanonicalForm());
+            if (i != size - 1) {
+                buf.append(", ");
+            }
+        }
+        buf.append("]");
+        return buf.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "[...]";
+    }
+    
+    @Override
+    boolean isLiteral() {
+        if (constantValue != null) {
+            return true;
+        }
+        for (int i = 0; i < items.size(); i++) {
+            ASTExpression exp = (ASTExpression) items.get(i);
+            if (!exp.isLiteral()) {
+                return false;
+            }
+        }
+        return true;
+    }
+    
+    // A hacky routine used by ASTDirVisit and ASTDirRecurse
+    TemplateSequenceModel evaluateStringsToNamespaces(Environment env) throws 
TemplateException {
+        TemplateSequenceModel val = (TemplateSequenceModel) eval(env);
+        NativeSequence result = new NativeSequence(val.size());
+        for (int i = 0; i < items.size(); i++) {
+            Object itemExpr = items.get(i);
+            if (itemExpr instanceof ASTExpStringLiteral) {
+                String s = ((ASTExpStringLiteral) itemExpr).getAsString();
+                try {
+                    Environment.Namespace ns = env.importLib(s, null);
+                    result.add(ns);
+                } catch (IOException ioe) {
+                    throw new _MiscTemplateException(((ASTExpStringLiteral) 
itemExpr),
+                            "Couldn't import library ", new _DelayedJQuote(s), 
": ",
+                            new _DelayedGetMessage(ioe));
+                }
+            } else {
+                result.add(val.get(i));
+            }
+        }
+        return result;
+    }
+    
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, 
ReplacemenetState replacementState) {
+               ArrayList clonedValues = (ArrayList) items.clone();
+               for (ListIterator iter = clonedValues.listIterator(); 
iter.hasNext(); ) {
+            iter.set(((ASTExpression) 
iter.next()).deepCloneWithIdentifierReplaced(
+                    replacedIdentifier, replacement, replacementState));
+        }
+        return new ASTExpListLiteral(clonedValues);
+    }
+
+    @Override
+    int getParameterCount() {
+        return items != null ? items.size() : 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        checkIndex(idx);
+        return items.get(idx);
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        checkIndex(idx);
+        return ParameterRole.ITEM_VALUE;
+    }
+
+    private void checkIndex(int idx) {
+        if (items == null || idx >= items.size()) {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java
new file mode 100644
index 0000000..86e376f
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpMethodCall.java
@@ -0,0 +1,147 @@
+/*
+ * 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.
+ */
+
+/*
+ * 22 October 1999: This class added by Holger Arendt.
+ */
+
+package org.apache.freemarker.core;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.util._NullWriter;
+
+
+/**
+ * AST expression node: {@code exp(args)}.
+ */
+final class ASTExpMethodCall extends ASTExpression {
+
+    private final ASTExpression target;
+    private final ASTExpListLiteral arguments;
+
+    ASTExpMethodCall(ASTExpression target, ArrayList arguments) {
+        this(target, new ASTExpListLiteral(arguments));
+    }
+
+    private ASTExpMethodCall(ASTExpression target, ASTExpListLiteral 
arguments) {
+        this.target = target;
+        this.arguments = arguments;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        TemplateModel targetModel = target.eval(env);
+        if (targetModel instanceof TemplateMethodModel) {
+            TemplateMethodModel targetMethod = (TemplateMethodModel) 
targetModel;
+            List argumentStrings = 
+            targetMethod instanceof TemplateMethodModelEx
+            ? arguments.getModelList(env)
+            : arguments.getValueList(env);
+            Object result = targetMethod.exec(argumentStrings);
+            return env.getObjectWrapper().wrap(result);
+        } else if (targetModel instanceof ASTDirMacro) {
+            ASTDirMacro func = (ASTDirMacro) targetModel;
+            env.setLastReturnValue(null);
+            if (!func.isFunction()) {
+                throw new _MiscTemplateException(env, "A macro cannot be 
called in an expression. (Functions can be.)");
+            }
+            Writer prevOut = env.getOut();
+            try {
+                env.setOut(_NullWriter.INSTANCE);
+                env.invoke(func, null, arguments.items, null, null);
+            } catch (IOException e) {
+                // Should not occur
+                throw new TemplateException("Unexpected exception during 
function execution", e, env);
+            } finally {
+                env.setOut(prevOut);
+            }
+            return env.getLastReturnValue();
+        } else {
+            throw new NonMethodException(target, targetModel, env);
+        }
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        StringBuilder buf = new StringBuilder();
+        buf.append(target.getCanonicalForm());
+        buf.append("(");
+        String list = arguments.getCanonicalForm();
+        buf.append(list.substring(1, list.length() - 1));
+        buf.append(")");
+        return buf.toString();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "...(...)";
+    }
+    
+    TemplateModel getConstantValue() {
+        return null;
+    }
+
+    @Override
+    boolean isLiteral() {
+        return false;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, 
ReplacemenetState replacementState) {
+        return new ASTExpMethodCall(
+                target.deepCloneWithIdentifierReplaced(replacedIdentifier, 
replacement, replacementState),
+                (ASTExpListLiteral) 
arguments.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, 
replacementState));
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1 + arguments.items.size();
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx == 0) {
+            return target;
+        } else if (idx < getParameterCount()) {
+            return arguments.items.get(idx - 1);
+        } else {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx == 0) {
+            return ParameterRole.CALLEE;
+        } else if (idx < getParameterCount()) {
+            return ParameterRole.ARGUMENT_VALUE;
+        } else {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java
new file mode 100644
index 0000000..a211ef7
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java
@@ -0,0 +1,110 @@
+/*
+ * 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 org.apache.freemarker.core.arithmetic.impl.ConservativeArithmeticEngine;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+
+/**
+ * AST expression node: {@code -exp} or {@code +exp}.
+ */
+final class ASTExpNegateOrPlus extends ASTExpression {
+    
+    private static final int TYPE_MINUS = 0;
+    private static final int TYPE_PLUS = 1;
+
+    private final ASTExpression target;
+    private final boolean isMinus;
+    private static final Integer MINUS_ONE = Integer.valueOf(-1); 
+
+    ASTExpNegateOrPlus(ASTExpression target, boolean isMinus) {
+        this.target = target;
+        this.isMinus = isMinus;
+    }
+    
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        TemplateNumberModel targetModel = null;
+        TemplateModel tm = target.eval(env);
+        try {
+            targetModel = (TemplateNumberModel) tm;
+        } catch (ClassCastException cce) {
+            throw new NonNumericalException(target, tm, env);
+        }
+        if (!isMinus) {
+            return targetModel;
+        }
+        target.assertNonNull(targetModel, env);
+        Number n = targetModel.getAsNumber();
+        // [FM3] Add ArithmeticEngine.negate, then use the engine from the env
+        n = ConservativeArithmeticEngine.INSTANCE.multiply(MINUS_ONE, n);
+        return new SimpleNumber(n);
+    }
+    
+    @Override
+    public String getCanonicalForm() {
+        String op = isMinus ? "-" : "+";
+        return op + target.getCanonicalForm();
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return isMinus ? "-..." : "+...";
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return target.isLiteral();
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, 
ReplacemenetState replacementState) {
+       return new ASTExpNegateOrPlus(
+               target.deepCloneWithIdentifierReplaced(replacedIdentifier, 
replacement, replacementState),
+               isMinus);
+    }
+
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return target;
+        case 1: return Integer.valueOf(isMinus ? TYPE_MINUS : TYPE_PLUS);
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.RIGHT_HAND_OPERAND;
+        case 1: return ParameterRole.AST_NODE_SUBTYPE;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNot.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNot.java 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNot.java
new file mode 100644
index 0000000..19dd088
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNot.java
@@ -0,0 +1,76 @@
+/*
+ * 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;
+
+/**
+ * AST expression node: {@code !exp}.
+ */
+final class ASTExpNot extends ASTExpBoolean {
+
+    private final ASTExpression target;
+
+    ASTExpNot(ASTExpression target) {
+        this.target = target;
+    }
+
+    @Override
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        return (!target.evalToBoolean(env));
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return "!" + target.getCanonicalForm();
+    }
+ 
+    @Override
+    String getNodeTypeSymbol() {
+        return "!";
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return target.isLiteral();
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, 
ReplacemenetState replacementState) {
+       return new ASTExpNot(
+               target.deepCloneWithIdentifierReplaced(replacedIdentifier, 
replacement, replacementState));
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return target;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.RIGHT_HAND_OPERAND;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java
new file mode 100644
index 0000000..01847a6
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java
@@ -0,0 +1,92 @@
+/*
+ * 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 org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+
+/**
+ * AST expression node: numerical literal
+ */
+final class ASTExpNumberLiteral extends ASTExpression implements 
TemplateNumberModel {
+
+    private final Number value;
+
+    public ASTExpNumberLiteral(Number value) {
+        this.value = value;
+    }
+    
+    @Override
+    TemplateModel _eval(Environment env) {
+        return new SimpleNumber(value);
+    }
+
+    @Override
+    public String evalAndCoerceToPlainText(Environment env) throws 
TemplateException {
+        return env.formatNumberToPlainText(this, this, false);
+    }
+
+    @Override
+    public Number getAsNumber() {
+        return value;
+    }
+    
+    String getName() {
+        return "the number: '" + value + "'";
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return value.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return getCanonicalForm();
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return true;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, 
ReplacemenetState replacementState) {
+        return new ASTExpNumberLiteral(value);
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpOr.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpOr.java 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpOr.java
new file mode 100644
index 0000000..5673ec3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpOr.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+
+/**
+ * AST expression node: {@code exp || exp}.
+ */
+final class ASTExpOr extends ASTExpBoolean {
+
+    private final ASTExpression lho;
+    private final ASTExpression rho;
+
+    ASTExpOr(ASTExpression lho, ASTExpression rho) {
+        this.lho = lho;
+        this.rho = rho;
+    }
+
+    @Override
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        return lho.evalToBoolean(env) || rho.evalToBoolean(env);
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return lho.getCanonicalForm() + " || " + rho.getCanonicalForm();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "||";
+    }
+
+    @Override
+    boolean isLiteral() {
+        return constantValue != null || (lho.isLiteral() && rho.isLiteral());
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, 
ReplacemenetState replacementState) {
+       return new ASTExpOr(
+               lho.deepCloneWithIdentifierReplaced(replacedIdentifier, 
replacement, replacementState),
+               rho.deepCloneWithIdentifierReplaced(replacedIdentifier, 
replacement, replacementState));
+    }
+
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return lho;
+        case 1: return rho;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.forBinaryOperatorOperand(idx);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java
new file mode 100644
index 0000000..eabccbf
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.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;
+
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * AST expression node: {@code (exp)}.
+ */
+final class ASTExpParenthesis extends ASTExpression {
+
+    private final ASTExpression nested;
+
+    ASTExpParenthesis(ASTExpression nested) {
+        this.nested = nested;
+    }
+
+    @Override
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        return nested.evalToBoolean(env);
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return "(" + nested.getCanonicalForm() + ")";
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "(...)";
+    }
+    
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        return nested.eval(env);
+    }
+    
+    @Override
+    public boolean isLiteral() {
+        return nested.isLiteral();
+    }
+    
+    ASTExpression getNestedExpression() {
+        return nested;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, 
ReplacemenetState replacementState) {
+        return new ASTExpParenthesis(
+                nested.deepCloneWithIdentifierReplaced(replacedIdentifier, 
replacement, replacementState));
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return nested;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        if (idx != 0) throw new IndexOutOfBoundsException();
+        return ParameterRole.ENCLOSED_OPERAND;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpRange.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpRange.java 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpRange.java
new file mode 100644
index 0000000..194c402
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpRange.java
@@ -0,0 +1,119 @@
+/*
+ * 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 org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.util.BugException;
+
+/**
+ * AST expression node: {@code exp .. exp}, {@code exp ..< exp} (or {@code exp 
..! exp}), {@code exp ..* exp}.
+ */
+final class ASTExpRange extends ASTExpression {
+
+    static final int END_INCLUSIVE = 0; 
+    static final int END_EXCLUSIVE = 1; 
+    static final int END_UNBOUND = 2; 
+    static final int END_SIZE_LIMITED = 3; 
+    
+    final ASTExpression lho;
+    final ASTExpression rho;
+    final int endType;
+
+    ASTExpRange(ASTExpression lho, ASTExpression rho, int endType) {
+        this.lho = lho;
+        this.rho = rho;
+        this.endType = endType;
+    }
+    
+    int getEndType() {
+        return endType;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        final int begin = lho.evalToNumber(env).intValue();
+        if (endType != END_UNBOUND) {
+            final int lhoValue = rho.evalToNumber(env).intValue();
+            return new BoundedRangeModel(
+                    begin, endType != END_SIZE_LIMITED ? lhoValue : begin + 
lhoValue,
+                    endType == END_INCLUSIVE, endType == END_SIZE_LIMITED); 
+        } else {
+            return new ListableRightUnboundedRangeModel(begin);
+        }
+    }
+    
+    // Surely this way we can tell that it won't be a boolean without 
evaluating the range, but why was this important?
+    @Override
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        throw new NonBooleanException(this, new BoundedRangeModel(0, 0, false, 
false), env);
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        String rhs = rho != null ? rho.getCanonicalForm() : "";
+        return lho.getCanonicalForm() + getNodeTypeSymbol() + rhs;
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        switch (endType) {
+        case END_EXCLUSIVE: return "..<";
+        case END_INCLUSIVE: return "..";
+        case END_UNBOUND: return "..";
+        case END_SIZE_LIMITED: return "..*";
+        default: throw new BugException(endType);
+        }
+    }
+    
+    @Override
+    boolean isLiteral() {
+        boolean rightIsLiteral = rho == null || rho.isLiteral();
+        return constantValue != null || (lho.isLiteral() && rightIsLiteral);
+    }
+    
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, 
ReplacemenetState replacementState) {
+        return new ASTExpRange(
+                lho.deepCloneWithIdentifierReplaced(replacedIdentifier, 
replacement, replacementState),
+                rho.deepCloneWithIdentifierReplaced(replacedIdentifier, 
replacement, replacementState),
+                endType);
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return lho;
+        case 1: return rho;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.forBinaryOperatorOperand(idx);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java
new file mode 100644
index 0000000..96c15df
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java
@@ -0,0 +1,211 @@
+/*
+ * 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.StringReader;
+import java.util.List;
+
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.util.FTLUtil;
+
+/**
+ * AST expression node: string literal
+ */
+final class ASTExpStringLiteral extends ASTExpression implements 
TemplateScalarModel {
+    
+    private final String value;
+    
+    /** {@link List} of {@link String}-s and {@link ASTInterpolation}-s. */
+    private List<Object> dynamicValue;
+    
+    ASTExpStringLiteral(String value) {
+        this.value = value;
+    }
+    
+    /**
+     * @param parentTkMan
+     *            The token source of the template that contains this string 
literal. As of this writing, we only need
+     *            this to share the {@code namingConvetion} with that.
+     */
+    void parseValue(FMParserTokenManager parentTkMan, OutputFormat 
outputFormat) throws ParseException {
+        // The way this works is incorrect (the literal should be parsed 
without un-escaping),
+        // but we can't fix this backward compatibly.
+        if (value.length() > 3 && (value.indexOf("${") >= 0 || 
value.indexOf("#{") >= 0)) {
+            
+            Template parentTemplate = getTemplate();
+            ParsingConfiguration pCfg = 
parentTemplate.getParsingConfiguration();
+
+            try {
+                SimpleCharStream simpleCharacterStream = new SimpleCharStream(
+                        new StringReader(value),
+                        beginLine, beginColumn + 1,
+                        value.length());
+                simpleCharacterStream.setTabSize(pCfg.getTabSize());
+                
+                FMParserTokenManager tkMan = new FMParserTokenManager(
+                        simpleCharacterStream);
+                
+                FMParser parser = new FMParser(parentTemplate, false,
+                        tkMan, pCfg, null, null,
+                        null);
+                // We continue from the parent parser's current state:
+                parser.setupStringLiteralMode(parentTkMan, outputFormat);
+                try {
+                    dynamicValue = parser.StaticTextAndInterpolations();
+                } finally {
+                    // The parent parser continues from this parser's current 
state:
+                    parser.tearDownStringLiteralMode(parentTkMan);
+                }
+            } catch (ParseException e) {
+                e.setTemplate(parentTemplate);
+                throw e;
+            }
+            constantValue = null;
+        }
+    }
+    
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        if (dynamicValue == null) {
+            return new SimpleScalar(value);
+        } else {
+            // This should behave like concatenating the values with `+`. 
Thus, an interpolated expression that
+            // returns markup promotes the result of the whole expression to 
markup.
+            
+            // Exactly one of these is non-null, depending on if the result 
will be plain text or markup, which can
+            // change during evaluation, depending on the result of the 
interpolations:
+            StringBuilder plainTextResult = null;
+            TemplateMarkupOutputModel<?> markupResult = null;
+            
+            for (Object part : dynamicValue) {
+                Object calcedPart =
+                        part instanceof String ? part
+                        : ((ASTInterpolation) 
part).calculateInterpolatedStringOrMarkup(env);
+                if (markupResult != null) {
+                    TemplateMarkupOutputModel<?> partMO = calcedPart 
instanceof String
+                            ? 
markupResult.getOutputFormat().fromPlainTextByEscaping((String) calcedPart)
+                            : (TemplateMarkupOutputModel<?>) calcedPart;
+                    markupResult = _EvalUtil.concatMarkupOutputs(this, 
markupResult, partMO);
+                } else { // We are using `plainTextOutput` (or nothing yet)
+                    if (calcedPart instanceof String) {
+                        String partStr = (String) calcedPart;
+                        if (plainTextResult == null) {
+                            plainTextResult = new StringBuilder(partStr);
+                        } else {
+                            plainTextResult.append(partStr);
+                        }
+                    } else { // `calcedPart` is TemplateMarkupOutputModel
+                        TemplateMarkupOutputModel<?> moPart = 
(TemplateMarkupOutputModel<?>) calcedPart;
+                        if (plainTextResult != null) {
+                            TemplateMarkupOutputModel<?> leftHandMO = 
moPart.getOutputFormat()
+                                    
.fromPlainTextByEscaping(plainTextResult.toString());
+                            markupResult = _EvalUtil.concatMarkupOutputs(this, 
leftHandMO, moPart);
+                            plainTextResult = null;
+                        } else {
+                            markupResult = moPart;
+                        }
+                    }
+                }
+            } // for each part
+            return markupResult != null ? markupResult
+                    : plainTextResult != null ? new 
SimpleScalar(plainTextResult.toString())
+                    : SimpleScalar.EMPTY_STRING;
+        }
+    }
+
+    @Override
+    public String getAsString() {
+        return value;
+    }
+    
+    /**
+     * Tells if this is something like <tt>"${foo}"</tt>, which is usually a 
user mistake.
+     */
+    boolean isSingleInterpolationLiteral() {
+        return dynamicValue != null && dynamicValue.size() == 1
+                && dynamicValue.get(0) instanceof ASTInterpolation;
+    }
+    
+    @Override
+    public String getCanonicalForm() {
+        if (dynamicValue == null) {
+            return FTLUtil.toStringLiteral(value);
+        } else {
+            StringBuilder sb = new StringBuilder();
+            sb.append('"');
+            for (Object child : dynamicValue) {
+                if (child instanceof ASTInterpolation) {
+                    sb.append(((ASTInterpolation) 
child).getCanonicalFormInStringLiteral());
+                } else {
+                    sb.append(FTLUtil.escapeStringLiteralPart((String) child, 
'"'));
+                }
+            }
+            sb.append('"');
+            return sb.toString();
+        }
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return dynamicValue == null ? getCanonicalForm() : "dynamic \"...\"";
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return dynamicValue == null;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, 
ReplacemenetState replacementState) {
+        ASTExpStringLiteral cloned = new ASTExpStringLiteral(value);
+        // FIXME: replacedIdentifier should be searched inside 
interpolatedOutput too:
+        cloned.dynamicValue = dynamicValue;
+        return cloned;
+    }
+
+    @Override
+    int getParameterCount() {
+        return dynamicValue == null ? 0 : dynamicValue.size();
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        checkIndex(idx);
+        return dynamicValue.get(idx);
+    }
+
+    private void checkIndex(int idx) {
+        if (dynamicValue == null || idx >= dynamicValue.size()) {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        checkIndex(idx);
+        return ParameterRole.VALUE_PART;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpVariable.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpVariable.java 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpVariable.java
new file mode 100644
index 0000000..59ceddc
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpVariable.java
@@ -0,0 +1,105 @@
+/*
+ * 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 org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST expression node: Reference to a "top-level" (local, current namespace, 
global, data-model) variable
+ */
+final class ASTExpVariable extends ASTExpression {
+
+    private final String name;
+
+    ASTExpVariable(String name) {
+        this.name = name;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        try {
+            return env.getVariable(name);
+        } catch (NullPointerException e) {
+            if (env == null) {
+                throw new _MiscTemplateException(
+                        "Variables are not available (certainly you are in a 
parse-time executed directive). "
+                        + "The name of the variable you tried to read: ", 
name);
+            } else {
+                throw e;
+            }
+        }
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return _StringUtil.toFTLTopLevelIdentifierReference(name);
+    }
+    
+    /**
+     * The name of the identifier without any escaping or other syntactical 
distortions. 
+     */
+    String getName() {
+        return name;
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return getCanonicalForm();
+    }
+
+    @Override
+    boolean isLiteral() {
+        return false;
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, 
ReplacemenetState replacementState) {
+        if (name.equals(replacedIdentifier)) {
+            if (replacementState.replacementAlreadyInUse) {
+                ASTExpression clone = 
replacement.deepCloneWithIdentifierReplaced(null, null, replacementState);
+                clone.copyLocationFrom(replacement);
+                return clone;
+            } else {
+                replacementState.replacementAlreadyInUse = true;
+                return replacement;
+            }
+        } else {
+            return new ASTExpVariable(name);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpression.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpression.java 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpression.java
new file mode 100644
index 0000000..be00f66
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpression.java
@@ -0,0 +1,208 @@
+/*
+ * 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 org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+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.model.impl.BeanModel;
+
+/**
+ * AST expression node superclass
+ */
+abstract class ASTExpression extends ASTNode {
+
+    /**
+     * @param env might be {@code null}, if this kind of expression can be 
evaluated during parsing (as opposed to
+     *     during template execution).
+     */
+    abstract TemplateModel _eval(Environment env) throws TemplateException;
+    
+    abstract boolean isLiteral();
+
+    // Used to store a constant return value for this expression. Only if it
+    // is possible, of course.
+    
+    TemplateModel constantValue;
+
+    // Hook in here to set the constant value if possible.
+    
+    @Override
+    void setLocation(Template template, int beginColumn, int beginLine, int 
endColumn, int endLine) {
+        super.setLocation(template, beginColumn, beginLine, endColumn, 
endLine);
+        if (isLiteral()) {
+            try {
+                constantValue = _eval(null);
+            } catch (Exception e) {
+            // deliberately ignore.
+            }
+        }
+    }
+
+    final TemplateModel getAsTemplateModel(Environment env) throws 
TemplateException {
+        return eval(env);
+    }
+    
+    final TemplateModel eval(Environment env) throws TemplateException {
+        return constantValue != null ? constantValue : _eval(env);
+    }
+    
+    String evalAndCoerceToPlainText(Environment env) throws TemplateException {
+        return _EvalUtil.coerceModelToPlainText(eval(env), this, null, env);
+    }
+
+    /**
+     * @param seqTip Tip to display if the value type is not coercable, but 
it's sequence or collection.
+     */
+    String evalAndCoerceToPlainText(Environment env, String seqTip) throws 
TemplateException {
+        return _EvalUtil.coerceModelToPlainText(eval(env), this, seqTip, env);
+    }
+
+    Object evalAndCoerceToStringOrMarkup(Environment env) throws 
TemplateException {
+        return _EvalUtil.coerceModelToStringOrMarkup(eval(env), this, null, 
env);
+    }
+
+    /**
+     * @param seqTip Tip to display if the value type is not coercable, but 
it's sequence or collection.
+     */
+    Object evalAndCoerceToStringOrMarkup(Environment env, String seqTip) 
throws TemplateException {
+        return _EvalUtil.coerceModelToStringOrMarkup(eval(env), this, seqTip, 
env);
+    }
+    
+    String evalAndCoerceToStringOrUnsupportedMarkup(Environment env) throws 
TemplateException {
+        return _EvalUtil.coerceModelToStringOrUnsupportedMarkup(eval(env), 
this, null, env);
+    }
+
+    /**
+     * @param seqTip Tip to display if the value type is not coercable, but 
it's sequence or collection.
+     */
+    String evalAndCoerceToStringOrUnsupportedMarkup(Environment env, String 
seqTip) throws TemplateException {
+        return _EvalUtil.coerceModelToStringOrUnsupportedMarkup(eval(env), 
this, seqTip, env);
+    }
+    
+    Number evalToNumber(Environment env) throws TemplateException {
+        TemplateModel model = eval(env);
+        return modelToNumber(model, env);
+    }
+
+    Number modelToNumber(TemplateModel model, Environment env) throws 
TemplateException {
+        if (model instanceof TemplateNumberModel) {
+            return _EvalUtil.modelToNumber((TemplateNumberModel) model, this);
+        } else {
+            throw new NonNumericalException(this, model, env);
+        }
+    }
+    
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        return evalToBoolean(env, null);
+    }
+
+    boolean evalToBoolean(Configuration cfg) throws TemplateException {
+        return evalToBoolean(null, cfg);
+    }
+
+    TemplateModel evalToNonMissing(Environment env) throws TemplateException {
+        TemplateModel result = eval(env);
+        assertNonNull(result, env);
+        return result;
+    }
+    
+    private boolean evalToBoolean(Environment env, Configuration cfg) throws 
TemplateException {
+        TemplateModel model = eval(env);
+        return modelToBoolean(model, env, cfg);
+    }
+    
+    boolean modelToBoolean(TemplateModel model, Environment env) throws 
TemplateException {
+        return modelToBoolean(model, env, null);
+    }
+
+    boolean modelToBoolean(TemplateModel model, Configuration cfg) throws 
TemplateException {
+        return modelToBoolean(model, null, cfg);
+    }
+    
+    private boolean modelToBoolean(TemplateModel model, Environment env, 
Configuration cfg) throws TemplateException {
+        if (model instanceof TemplateBooleanModel) {
+            return ((TemplateBooleanModel) model).getAsBoolean();
+        } else {
+            throw new NonBooleanException(this, model, env);
+        }
+    }
+    
+    final ASTExpression deepCloneWithIdentifierReplaced(
+            String replacedIdentifier, ASTExpression replacement, 
ReplacemenetState replacementState) {
+        ASTExpression clone = 
deepCloneWithIdentifierReplaced_inner(replacedIdentifier, replacement, 
replacementState);
+        if (clone.beginLine == 0) {
+            clone.copyLocationFrom(this);
+        }
+        return clone;
+    }
+    
+    static class ReplacemenetState {
+        /**
+         * If the replacement expression is not in use yet, we don't have to 
deepClone it.
+         */
+        boolean replacementAlreadyInUse; 
+    }
+
+    /**
+     * This should return an equivalent new expression object (or an 
identifier replacement expression).
+     * The position need not be filled, unless it will be different from the 
position of what we were cloning. 
+     */
+    protected abstract ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, 
ReplacemenetState replacementState);
+
+    static boolean isEmpty(TemplateModel model) throws TemplateModelException {
+        if (model instanceof BeanModel) {
+            return ((BeanModel) model).isEmpty();
+        } else if (model instanceof TemplateSequenceModel) {
+            return ((TemplateSequenceModel) model).size() == 0;
+        } else if (model instanceof TemplateScalarModel) {
+            String s = ((TemplateScalarModel) model).getAsString();
+            return (s == null || s.length() == 0);
+        } else if (model == null) {
+            return true;
+        } else if (model instanceof TemplateMarkupOutputModel) { // Note: 
happens just after FTL string check
+            TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel) model;
+            return mo.getOutputFormat().isEmpty(mo);
+        } else if (model instanceof TemplateCollectionModel) {
+            return !((TemplateCollectionModel) model).iterator().hasNext();
+        } else if (model instanceof TemplateHashModel) {
+            return ((TemplateHashModel) model).isEmpty();
+        } else if (model instanceof TemplateNumberModel
+                || model instanceof TemplateDateModel
+                || model instanceof TemplateBooleanModel) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+    
+    void assertNonNull(TemplateModel model, Environment env) throws 
InvalidReferenceException {
+        if (model == null) throw InvalidReferenceException.getInstance(this, 
env);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java
new file mode 100644
index 0000000..8c3f8fa
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java
@@ -0,0 +1,172 @@
+/*
+ * 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.io.Writer;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+import org.apache.freemarker.core.util.FTLUtil;
+
+/**
+ * AST interpolation node: <tt>#{exp}</tt>
+ */
+final class ASTHashInterpolation extends ASTInterpolation {
+
+    private final ASTExpression expression;
+    private final boolean hasFormat;
+    private final int minFracDigits;
+    private final int maxFracDigits;
+    /** For OutputFormat-based auto-escaping */
+    private final MarkupOutputFormat autoEscapeOutputFormat;
+    private volatile FormatHolder formatCache; // creating new NumberFormat is 
slow operation
+
+    ASTHashInterpolation(ASTExpression expression, MarkupOutputFormat 
autoEscapeOutputFormat) {
+        this.expression = expression;
+        hasFormat = false;
+        minFracDigits = 0;
+        maxFracDigits = 0;
+        this.autoEscapeOutputFormat = autoEscapeOutputFormat;
+    }
+
+    ASTHashInterpolation(ASTExpression expression,
+            int minFracDigits, int maxFracDigits,
+            MarkupOutputFormat autoEscapeOutputFormat) {
+        this.expression = expression;
+        hasFormat = true;
+        this.minFracDigits = minFracDigits;
+        this.maxFracDigits = maxFracDigits;
+        this.autoEscapeOutputFormat = autoEscapeOutputFormat;
+    }
+
+    @Override
+    ASTElement[] accept(Environment env) throws TemplateException, IOException 
{
+        String s = calculateInterpolatedStringOrMarkup(env);
+        Writer out = env.getOut();
+        if (autoEscapeOutputFormat != null) {
+            autoEscapeOutputFormat.output(s, out);
+        } else {
+            out.write(s);
+        }
+        return null;
+    }
+
+    @Override
+    protected String calculateInterpolatedStringOrMarkup(Environment env) 
throws TemplateException {
+        Number num = expression.evalToNumber(env);
+        
+        FormatHolder fmth = formatCache;  // atomic sampling
+        if (fmth == null || !fmth.locale.equals(env.getLocale())) {
+            synchronized (this) {
+                fmth = formatCache;
+                if (fmth == null || !fmth.locale.equals(env.getLocale())) {
+                    NumberFormat fmt = 
NumberFormat.getNumberInstance(env.getLocale());
+                    if (hasFormat) {
+                        fmt.setMinimumFractionDigits(minFracDigits);
+                        fmt.setMaximumFractionDigits(maxFracDigits);
+                    } else {
+                        fmt.setMinimumFractionDigits(0);
+                        fmt.setMaximumFractionDigits(50);
+                    }
+                    fmt.setGroupingUsed(false);
+                    formatCache = new FormatHolder(fmt, env.getLocale());
+                    fmth = formatCache;
+                }
+            }
+        }
+        // We must use Format even if hasFormat == false.
+        // Some locales may use non-Arabic digits, thus replacing the
+        // decimal separator in the result of toString() is not enough.
+        return fmth.format.format(num);
+    }
+
+    @Override
+    protected String dump(boolean canonical, boolean inStringLiteral) {
+        StringBuilder buf = new StringBuilder("#{");
+        final String exprCF = expression.getCanonicalForm();
+        buf.append(inStringLiteral ? FTLUtil.escapeStringLiteralPart(exprCF, 
'"') : exprCF);
+        if (hasFormat) {
+            buf.append(" ; ");
+            buf.append("m");
+            buf.append(minFracDigits);
+            buf.append("M");
+            buf.append(maxFracDigits);
+        }
+        buf.append("}");
+        return buf.toString();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#{...}";
+    }
+
+    @Override
+    boolean heedsOpeningWhitespace() {
+        return true;
+    }
+
+    @Override
+    boolean heedsTrailingWhitespace() {
+        return true;
+    }
+    
+    private static class FormatHolder {
+        final NumberFormat format;
+        final Locale locale;
+        
+        FormatHolder(NumberFormat format, Locale locale) {
+            this.format = format;
+            this.locale = locale;
+        }
+    }
+
+    @Override
+    int getParameterCount() {
+        return 3;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return expression;
+        case 1: return Integer.valueOf(minFracDigits);
+        case 2: return Integer.valueOf(maxFracDigits);
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.CONTENT;
+        case 1: return ParameterRole.MINIMUM_DECIMALS;
+        case 2: return ParameterRole.MAXIMUM_DECIMALS;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java
new file mode 100644
index 0000000..4d3c339
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java
@@ -0,0 +1,101 @@
+/*
+ * 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;
+
+/**
+ * AST directive-like node, used where there's no other parent for a list of 
{@link ASTElement}-s. Most often occurs as
+ * the root node of the AST.
+ */
+final class ASTImplicitParent extends ASTElement {
+
+    ASTImplicitParent() { }
+    
+    @Override
+    ASTElement postParseCleanup(boolean stripWhitespace)
+        throws ParseException {
+        super.postParseCleanup(stripWhitespace);
+        return getChildCount() == 1 ? getChild(0) : this;
+    }
+
+    /**
+     * Processes the contents of the internal <tt>ASTElement</tt> list,
+     * and outputs the resulting text.
+     */
+    @Override
+    ASTElement[] accept(Environment env)
+        throws TemplateException, IOException {
+        return getChildBuffer();
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        if (canonical) {
+            return getChildrenCanonicalForm();
+        } else {
+            if (getParent() == null) {
+                return "root";
+            }
+            return getNodeTypeSymbol(); // ASTImplicitParent is uninteresting 
in a stack trace.
+        }
+    }
+
+    @Override
+    protected boolean isOutputCacheable() {
+        int ln = getChildCount();
+        for (int i = 0; i < ln; i++) {
+            if (!getChild(i).isOutputCacheable()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return "#mixed_content";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+    
+    @Override
+    boolean isIgnorable(boolean stripWhitespace) {
+        return getChildCount() == 0;
+    }
+
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTInterpolation.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTInterpolation.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTInterpolation.java
new file mode 100644
index 0000000..028acc2
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTInterpolation.java
@@ -0,0 +1,51 @@
+/*
+ * 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 org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+
+/**
+ * AST interpolation node superclass.
+ */
+abstract class ASTInterpolation extends ASTElement {
+
+    protected abstract String dump(boolean canonical, boolean inStringLiteral);
+
+    @Override
+    protected final String dump(boolean canonical) {
+        return dump(canonical, false);
+    }
+    
+    final String getCanonicalFormInStringLiteral() {
+        return dump(true, true);
+    }
+
+    /**
+     * Returns the already type-converted value that this interpolation will 
insert into the output.
+     * 
+     * @return A {@link String} or {@link TemplateMarkupOutputModel}. Not 
{@code null}.
+     */
+    protected abstract Object calculateInterpolatedStringOrMarkup(Environment 
env) throws TemplateException;
+
+    @Override
+    boolean isShownInStackTrace() {
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTNode.java
----------------------------------------------------------------------
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTNode.java 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTNode.java
new file mode 100644
index 0000000..18e34c1
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTNode.java
@@ -0,0 +1,233 @@
+/*
+ * 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;
+
+/**
+ * AST node: The superclass of all AST nodes
+ */
+abstract class ASTNode {
+    
+    private Template template;
+    int beginColumn, beginLine, endColumn, endLine;
+    
+    /** This is needed for an ?eval hack; the expression AST nodes will be the 
descendants of the template, however,
+     *  we can't give their position in the template, only in the dynamic 
string that's evaluated. That's signaled
+     *  by a negative line numbers, starting from this constant as line 1. */
+    static final int RUNTIME_EVAL_LINE_DISPLACEMENT = -1000000000;  
+
+    final void setLocation(Template template, Token begin, Token end) {
+        setLocation(template, begin.beginColumn, begin.beginLine, 
end.endColumn, end.endLine);
+    }
+
+    final void setLocation(Template template, Token tagBegin, Token tagEnd, 
TemplateElements children) {
+        ASTElement lastChild = children.getLast();
+        if (lastChild != null) {
+            // [<#if exp>children]<#else>
+            setLocation(template, tagBegin, lastChild);
+        } else {
+            // [<#if exp>]<#else>
+            setLocation(template, tagBegin, tagEnd);
+        }
+    }
+    
+    final void setLocation(Template template, Token begin, ASTNode end) {
+        setLocation(template, begin.beginColumn, begin.beginLine, 
end.endColumn, end.endLine);
+    }
+    
+    final void setLocation(Template template, ASTNode begin, Token end) {
+        setLocation(template, begin.beginColumn, begin.beginLine, 
end.endColumn, end.endLine);
+    }
+
+    final void setLocation(Template template, ASTNode begin, ASTNode end) {
+        setLocation(template, begin.beginColumn, begin.beginLine, 
end.endColumn, end.endLine);
+    }
+
+    void setLocation(Template template, int beginColumn, int beginLine, int 
endColumn, int endLine) {
+        this.template = template;
+        this.beginColumn = beginColumn;
+        this.beginLine = beginLine;
+        this.endColumn = endColumn;
+        this.endLine = endLine;
+    }
+    
+    public final int getBeginColumn() {
+        return beginColumn;
+    }
+
+    public final int getBeginLine() {
+        return beginLine;
+    }
+
+    public final int getEndColumn() {
+        return endColumn;
+    }
+
+    public final int getEndLine() {
+        return endLine;
+    }
+
+    /**
+     * Returns a string that indicates
+     * where in the template source, this object is.
+     */
+    public String getStartLocation() {
+        return MessageUtil.formatLocationForEvaluationError(template, 
beginLine, beginColumn);
+    }
+
+    /**
+     * As of 2.3.20. the same as {@link #getStartLocation}. Meant to be used 
where there's a risk of XSS
+     * when viewing error messages.
+     */
+    public String getStartLocationQuoted() {
+        return getStartLocation();
+    }
+
+    public String getEndLocation() {
+        return MessageUtil.formatLocationForEvaluationError(template, endLine, 
endColumn);
+    }
+
+    /**
+     * As of 2.3.20. the same as {@link #getEndLocation}. Meant to be used 
where there's a risk of XSS
+     * when viewing error messages.
+     */
+    public String getEndLocationQuoted() {
+        return getEndLocation();
+    }
+    
+    public final String getSource() {
+        String s;
+        if (template != null) {
+            s = template.getSource(beginColumn, beginLine, endColumn, endLine);
+        } else {
+            s = null;
+        }
+
+        // Can't just return null for backward-compatibility... 
+        return s != null ? s : getCanonicalForm();
+    }
+
+    @Override
+    public String toString() {
+        String s;
+       try {
+               s = getSource();
+       } catch (Exception e) { // REVISIT: A bit of a hack? (JR)
+           s = null;
+       }
+       return s != null ? s : getCanonicalForm();
+    }
+
+    /**
+     * @return whether the point in the template file specified by the 
+     * column and line numbers is contained within this template object.
+     */
+    public boolean contains(int column, int line) {
+        if (line < beginLine || line > endLine) {
+            return false;
+        }
+        if (line == beginLine) {
+            if (column < beginColumn) {
+                return false;
+            }
+        }
+        if (line == endLine) {
+            if (column > endColumn) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public Template getTemplate() {
+        return template;
+    }
+    
+    ASTNode copyLocationFrom(ASTNode from) {
+        template = from.template;
+        beginColumn = from.beginColumn;
+        beginLine = from.beginLine;
+        endColumn = from.endColumn;
+        endLine = from.endLine;
+        return this;
+    }    
+
+    /**
+     * FTL generated from the AST of the node, which must be parseable to an 
AST that does the same as the original
+     * source, assuming we turn off automatic white-space removal when parsing 
the canonical form.
+     * 
+     * @see ASTElement#getDescription()
+     * @see #getNodeTypeSymbol()
+     */
+    abstract public String getCanonicalForm();
+    
+    /**
+     * A very sort single-line string that describes what kind of AST node 
this is, without describing any 
+     * embedded expression or child element. Examples: {@code "#if"}, {@code 
"+"}, <tt>"${...}</tt>. These values should
+     * be suitable as tree node labels in a tree view. Yet, they should be 
consistent and complete enough so that an AST
+     * that is equivalent with the original could be reconstructed from the 
tree view. Thus, for literal values that are
+     * leaf nodes the symbols should be the canonical form of value.
+     * 
+     * Note that {@link ASTElement#getDescription()} has similar role, only it 
doesn't go under the element level
+     * (i.e. down to the expression level), instead it always prints the 
embedded expressions itself.
+     * 
+     * @see #getCanonicalForm()
+     * @see ASTElement#getDescription()
+     */
+    abstract String getNodeTypeSymbol();
+    
+    /**
+     * Returns highest valid parameter index + 1. So one should scan indexes 
with {@link #getParameterValue(int)}
+     * starting from 0 up until but excluding this. For example, for the 
binary "+" operator this will give 2, so the
+     * legal indexes are 0 and 1. Note that if a parameter is optional in a 
template-object-type and happens to be
+     * omitted in an instance, this will still return the same value and the 
value of that parameter will be
+     * {@code null}.
+     */
+    abstract int getParameterCount();
+    
+    /**
+     * Returns the value of the parameter identified by the index. For 
example, the binary "+" operator will have an
+     * LHO {@link ASTExpression} at index 0, and and RHO {@link ASTExpression} 
at index 1. Or, the binary "." operator will
+     * have an LHO {@link ASTExpression} at index 0, and an RHO {@link 
String}(!) at index 1. Or, the {@code #include}
+     * directive will have a path {@link ASTExpression} at index 0, a "parse" 
{@link ASTExpression} at index 1, etc.
+     * 
+     * <p>The index value doesn't correspond to the source-code location in 
general. It's an arbitrary identifier
+     * that corresponds to the role of the parameter instead. This also means 
that when a parameter is omitted, the
+     * index of the other parameters won't shift.
+     *
+     *  @return {@code null} or any kind of {@link Object}, very often an 
{@link ASTExpression}. However, if there's
+     *      a {@link ASTNode} stored inside the returned value, it must itself 
be be a {@link ASTNode}
+     *      too, otherwise the AST couldn't be (easily) fully traversed. That 
is, non-{@link ASTNode} values
+     *      can only be used for leafs. 
+     *  
+     *  @throws IndexOutOfBoundsException if {@code idx} is less than 0 or not 
less than {@link #getParameterCount()}. 
+     */
+    abstract Object getParameterValue(int idx);
+
+    /**
+     *  Returns the role of the parameter at the given index, like {@link 
ParameterRole#LEFT_HAND_OPERAND}.
+     *  
+     *  As of this writing (2013-06-17), for directive parameters it will 
always give {@link ParameterRole#UNKNOWN},
+     *  because there was no need to be more specific so far. This should be 
improved as need.
+     *  
+     *  @throws IndexOutOfBoundsException if {@code idx} is less than 0 or not 
less than {@link #getParameterCount()}. 
+     */
+    abstract ParameterRole getParameterRole(int idx);
+    
+}

Reply via email to