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/05/14 10:53:28 UTC

[45/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java
new file mode 100644
index 0000000..15e632a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java
@@ -0,0 +1,313 @@
+/*
+ * 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.HashSet;
+import java.util.Set;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+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.TemplateModelIterator;
+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.CollectionAndSequence;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+
+/**
+ * AST expression node: binary {@code +} operator. Note that this is treated separately from the other 4 arithmetic
+ * operators, since it's overloaded to mean concatenation of string-s, sequences and hash-es too.
+ */
+final class ASTExpAddOrConcat extends ASTExpression {
+
+    private final ASTExpression left;
+    private final ASTExpression right;
+
+    ASTExpAddOrConcat(ASTExpression left, ASTExpression right) {
+        this.left = left;
+        this.right = right;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        return _eval(env, this, left, left.eval(env), right, right.eval(env));
+    }
+
+    /**
+     * @param leftExp
+     *            Used for error messages only; can be {@code null}
+     * @param rightExp
+     *            Used for error messages only; can be {@code null}
+     */
+    static TemplateModel _eval(Environment env,
+            ASTNode parent,
+            ASTExpression leftExp, TemplateModel leftModel,
+            ASTExpression rightExp, TemplateModel rightModel)
+            throws TemplateException {
+        if (leftModel instanceof TemplateNumberModel && rightModel instanceof TemplateNumberModel) {
+            Number first = _EvalUtil.modelToNumber((TemplateNumberModel) leftModel, leftExp);
+            Number second = _EvalUtil.modelToNumber((TemplateNumberModel) rightModel, rightExp);
+            return _evalOnNumbers(env, parent, first, second);
+        } else if (leftModel instanceof TemplateSequenceModel && rightModel instanceof TemplateSequenceModel) {
+            return new ConcatenatedSequence((TemplateSequenceModel) leftModel, (TemplateSequenceModel) rightModel);
+        } else {
+            boolean hashConcatPossible
+                    = leftModel instanceof TemplateHashModel && rightModel instanceof TemplateHashModel;
+            try {
+                // We try string addition first. If hash addition is possible, then instead of throwing exception
+                // we return null and do hash addition instead. (We can't simply give hash addition a priority, like
+                // with sequence addition above, as FTL strings are often also FTL hashes.)
+                Object leftOMOrStr = _EvalUtil.coerceModelToStringOrMarkup(
+                        leftModel, leftExp, /* returnNullOnNonCoercableType = */ hashConcatPossible, null,
+                        env);
+                if (leftOMOrStr == null) {
+                    return _eval_concatenateHashes(leftModel, rightModel);
+                }
+
+                // Same trick with null return as above.
+                Object rightOMOrStr = _EvalUtil.coerceModelToStringOrMarkup(
+                        rightModel, rightExp, /* returnNullOnNonCoercableType = */ hashConcatPossible, null,
+                        env);
+                if (rightOMOrStr == null) {
+                    return _eval_concatenateHashes(leftModel, rightModel);
+                }
+
+                if (leftOMOrStr instanceof String) {
+                    if (rightOMOrStr instanceof String) {
+                        return new SimpleScalar(((String) leftOMOrStr).concat((String) rightOMOrStr));
+                    } else { // rightOMOrStr instanceof TemplateMarkupOutputModel
+                        TemplateMarkupOutputModel<?> rightMO = (TemplateMarkupOutputModel<?>) rightOMOrStr; 
+                        return _EvalUtil.concatMarkupOutputs(parent,
+                                rightMO.getOutputFormat().fromPlainTextByEscaping((String) leftOMOrStr),
+                                rightMO);
+                    }                    
+                } else { // leftOMOrStr instanceof TemplateMarkupOutputModel 
+                    TemplateMarkupOutputModel<?> leftMO = (TemplateMarkupOutputModel<?>) leftOMOrStr; 
+                    if (rightOMOrStr instanceof String) {  // markup output
+                        return _EvalUtil.concatMarkupOutputs(parent,
+                                leftMO,
+                                leftMO.getOutputFormat().fromPlainTextByEscaping((String) rightOMOrStr));
+                    } else { // rightOMOrStr instanceof TemplateMarkupOutputModel
+                        return _EvalUtil.concatMarkupOutputs(parent,
+                                leftMO,
+                                (TemplateMarkupOutputModel<?>) rightOMOrStr);
+                    }
+                }
+            } catch (NonStringOrTemplateOutputException e) {
+                // 2.4: Remove this catch; it's for BC, after reworking hash addition so it doesn't rely on this. But
+                // user code might throws this (very unlikely), and then in 2.3.x we did catch that too, incorrectly.
+                if (hashConcatPossible) {
+                    return _eval_concatenateHashes(leftModel, rightModel);
+                } else {
+                    throw e;
+                }
+            }
+        }
+    }
+
+    private static TemplateModel _eval_concatenateHashes(TemplateModel leftModel, TemplateModel rightModel)
+            throws TemplateModelException {
+        if (leftModel instanceof TemplateHashModelEx && rightModel instanceof TemplateHashModelEx) {
+            TemplateHashModelEx leftModelEx = (TemplateHashModelEx) leftModel;
+            TemplateHashModelEx rightModelEx = (TemplateHashModelEx) rightModel;
+            if (leftModelEx.size() == 0) {
+                return rightModelEx;
+            } else if (rightModelEx.size() == 0) {
+                return leftModelEx;
+            } else {
+                return new ConcatenatedHashEx(leftModelEx, rightModelEx);
+            }
+        } else {
+            return new ConcatenatedHash((TemplateHashModel) leftModel,
+                                        (TemplateHashModel) rightModel);
+        }
+    }
+
+    static TemplateModel _evalOnNumbers(Environment env, ASTNode parent, Number first, Number second)
+            throws TemplateException {
+        ArithmeticEngine ae = _EvalUtil.getArithmeticEngine(env, parent);
+        return new SimpleNumber(ae.add(first, second));
+    }
+
+    @Override
+    boolean isLiteral() {
+        return constantValue != null || (left.isLiteral() && right.isLiteral());
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpAddOrConcat(
+    	left.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	right.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return left.getCanonicalForm() + " + " + right.getCanonicalForm();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "+";
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        return idx == 0 ? left : right;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.forBinaryOperatorOperand(idx);
+    }
+
+    private static final class ConcatenatedSequence
+    implements
+        TemplateSequenceModel {
+        private final TemplateSequenceModel left;
+        private final TemplateSequenceModel right;
+
+        ConcatenatedSequence(TemplateSequenceModel left, TemplateSequenceModel right) {
+            this.left = left;
+            this.right = right;
+        }
+
+        @Override
+        public int size()
+        throws TemplateModelException {
+            return left.size() + right.size();
+        }
+
+        @Override
+        public TemplateModel get(int i)
+        throws TemplateModelException {
+            int ls = left.size();
+            return i < ls ? left.get(i) : right.get(i - ls);
+        }
+    }
+
+    private static class ConcatenatedHash
+    implements TemplateHashModel {
+        protected final TemplateHashModel left;
+        protected final TemplateHashModel right;
+
+        ConcatenatedHash(TemplateHashModel left, TemplateHashModel right) {
+            this.left = left;
+            this.right = right;
+        }
+        
+        @Override
+        public TemplateModel get(String key)
+        throws TemplateModelException {
+            TemplateModel model = right.get(key);
+            return (model != null) ? model : left.get(key);
+        }
+
+        @Override
+        public boolean isEmpty()
+        throws TemplateModelException {
+            return left.isEmpty() && right.isEmpty();
+        }
+    }
+
+    private static final class ConcatenatedHashEx
+    extends ConcatenatedHash
+    implements TemplateHashModelEx {
+        private CollectionAndSequence keys;
+        private CollectionAndSequence values;
+        private int size;
+
+        ConcatenatedHashEx(TemplateHashModelEx left, TemplateHashModelEx right) {
+            super(left, right);
+        }
+        
+        @Override
+        public int size() throws TemplateModelException {
+            initKeys();
+            return size;
+        }
+
+        @Override
+        public TemplateCollectionModel keys()
+        throws TemplateModelException {
+            initKeys();
+            return keys;
+        }
+
+        @Override
+        public TemplateCollectionModel values()
+        throws TemplateModelException {
+            initValues();
+            return values;
+        }
+
+        private void initKeys()
+        throws TemplateModelException {
+            if (keys == null) {
+                HashSet keySet = new HashSet();
+                NativeSequence keySeq = new NativeSequence(32);
+                addKeys(keySet, keySeq, (TemplateHashModelEx) left);
+                addKeys(keySet, keySeq, (TemplateHashModelEx) right);
+                size = keySet.size();
+                keys = new CollectionAndSequence(keySeq);
+            }
+        }
+
+        private static void addKeys(Set set, NativeSequence keySeq, TemplateHashModelEx hash)
+        throws TemplateModelException {
+            TemplateModelIterator it = hash.keys().iterator();
+            while (it.hasNext()) {
+                TemplateScalarModel tsm = (TemplateScalarModel) it.next();
+                if (set.add(tsm.getAsString())) {
+                    // The first occurence of the key decides the index;
+                    // this is consisten with stuff like java.util.LinkedHashSet.
+                    keySeq.add(tsm);
+                }
+            }
+        }        
+
+        private void initValues()
+        throws TemplateModelException {
+            if (values == null) {
+                NativeSequence seq = new NativeSequence(size());
+                // Note: size() invokes initKeys() if needed.
+            
+                int ln = keys.size();
+                for (int i  = 0; i < ln; i++) {
+                    seq.add(get(((TemplateScalarModel) keys.get(i)).getAsString()));
+                }
+                values = new CollectionAndSequence(seq);
+            }
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAnd.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAnd.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAnd.java
new file mode 100644
index 0000000..346d526
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAnd.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 &&} operator
+ */
+final class ASTExpAnd extends ASTExpBoolean {
+
+    private final ASTExpression lho;
+    private final ASTExpression rho;
+
+    ASTExpAnd(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 ASTExpAnd(
+    	        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);
+    }
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBoolean.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBoolean.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBoolean.java
new file mode 100644
index 0000000..d580372
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBoolean.java
@@ -0,0 +1,34 @@
+/*
+ * 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.TemplateModel;
+
+/**
+ * AST expression node superclass for expressions returning a boolean.
+ */
+abstract class ASTExpBoolean extends ASTExpression {
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        return evalToBoolean(env) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBooleanLiteral.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBooleanLiteral.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBooleanLiteral.java
new file mode 100644
index 0000000..e38578b
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBooleanLiteral.java
@@ -0,0 +1,91 @@
+/*
+ * 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.TemplateModel;
+
+/**
+ * AST expression node: boolean literal 
+ */
+final class ASTExpBooleanLiteral extends ASTExpression {
+
+    private final boolean val;
+
+    public ASTExpBooleanLiteral(boolean val) {
+        this.val = val;
+    }
+
+    static TemplateBooleanModel getTemplateModel(boolean b) {
+        return b? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+    }
+
+    @Override
+    boolean evalToBoolean(Environment env) {
+        return val;
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return val ? MiscUtil.C_TRUE : MiscUtil.C_FALSE;
+    }
+
+    @Override
+    String getNodeTypeSymbol() {
+        return getCanonicalForm();
+    }
+    
+    @Override
+    public String toString() {
+        return val ? MiscUtil.C_TRUE : MiscUtil.C_FALSE;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) {
+        return val ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return true;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpBooleanLiteral(val);
+    }
+    
+    @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/ASTExpBuiltIn.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
new file mode 100644
index 0000000..be559f6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
@@ -0,0 +1,485 @@
+/*
+ * 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.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.freemarker.core.BuiltInsForDates.iso_BI;
+import org.apache.freemarker.core.BuiltInsForDates.iso_utc_or_local_BI;
+import org.apache.freemarker.core.BuiltInsForMarkupOutputs.markup_stringBI;
+import org.apache.freemarker.core.BuiltInsForMultipleTypes.is_dateLikeBI;
+import org.apache.freemarker.core.BuiltInsForNodes.ancestorsBI;
+import org.apache.freemarker.core.BuiltInsForNodes.childrenBI;
+import org.apache.freemarker.core.BuiltInsForNodes.nextSiblingBI;
+import org.apache.freemarker.core.BuiltInsForNodes.node_nameBI;
+import org.apache.freemarker.core.BuiltInsForNodes.node_namespaceBI;
+import org.apache.freemarker.core.BuiltInsForNodes.node_typeBI;
+import org.apache.freemarker.core.BuiltInsForNodes.parentBI;
+import org.apache.freemarker.core.BuiltInsForNodes.previousSiblingBI;
+import org.apache.freemarker.core.BuiltInsForNodes.rootBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.absBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.byteBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.ceilingBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.doubleBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.floatBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.floorBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.intBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.is_infiniteBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.is_nanBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.longBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.number_to_dateBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.roundBI;
+import org.apache.freemarker.core.BuiltInsForNumbers.shortBI;
+import org.apache.freemarker.core.BuiltInsForOutputFormatRelated.escBI;
+import org.apache.freemarker.core.BuiltInsForOutputFormatRelated.no_escBI;
+import org.apache.freemarker.core.BuiltInsForSequences.chunkBI;
+import org.apache.freemarker.core.BuiltInsForSequences.firstBI;
+import org.apache.freemarker.core.BuiltInsForSequences.lastBI;
+import org.apache.freemarker.core.BuiltInsForSequences.reverseBI;
+import org.apache.freemarker.core.BuiltInsForSequences.seq_containsBI;
+import org.apache.freemarker.core.BuiltInsForSequences.seq_index_ofBI;
+import org.apache.freemarker.core.BuiltInsForSequences.sortBI;
+import org.apache.freemarker.core.BuiltInsForSequences.sort_byBI;
+import org.apache.freemarker.core.BuiltInsForStringsMisc.evalBI;
+import org.apache.freemarker.core.model.TemplateDateModel;
+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.util._DateUtil;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST expression node: {@code exp?name}
+ */
+abstract class ASTExpBuiltIn extends ASTExpression implements Cloneable {
+    
+    protected ASTExpression target;
+    protected String key;
+
+    static final Set<String> CAMEL_CASE_NAMES = new TreeSet<>();
+    static final Set<String> SNAKE_CASE_NAMES = new TreeSet<>();
+    static final int NUMBER_OF_BIS = 263;
+    static final HashMap<String, ASTExpBuiltIn> BUILT_INS_BY_NAME = new HashMap(NUMBER_OF_BIS * 3 / 2 + 1, 1f);
+
+    static {
+        // Note that you must update NUMBER_OF_BIS if you add new items here!
+        
+        putBI("abs", new absBI());
+        putBI("ancestors", new ancestorsBI());
+        putBI("api", new BuiltInsForMultipleTypes.apiBI());
+        putBI("boolean", new BuiltInsForStringsMisc.booleanBI());
+        putBI("byte", new byteBI());
+        putBI("c", new BuiltInsForMultipleTypes.cBI());
+        putBI("cap_first", "capFirst", new BuiltInsForStringsBasic.cap_firstBI());
+        putBI("capitalize", new BuiltInsForStringsBasic.capitalizeBI());
+        putBI("ceiling", new ceilingBI());
+        putBI("children", new childrenBI());
+        putBI("chop_linebreak", "chopLinebreak", new BuiltInsForStringsBasic.chop_linebreakBI());
+        putBI("contains", new BuiltInsForStringsBasic.containsBI());        
+        putBI("date", new BuiltInsForMultipleTypes.dateBI(TemplateDateModel.DATE));
+        putBI("date_if_unknown", "dateIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.DATE));
+        putBI("datetime", new BuiltInsForMultipleTypes.dateBI(TemplateDateModel.DATETIME));
+        putBI("datetime_if_unknown", "datetimeIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.DATETIME));
+        putBI("default", new BuiltInsForExistenceHandling.defaultBI());
+        putBI("double", new doubleBI());
+        putBI("ends_with", "endsWith", new BuiltInsForStringsBasic.ends_withBI());
+        putBI("ensure_ends_with", "ensureEndsWith", new BuiltInsForStringsBasic.ensure_ends_withBI());
+        putBI("ensure_starts_with", "ensureStartsWith", new BuiltInsForStringsBasic.ensure_starts_withBI());
+        putBI("esc", new escBI());
+        putBI("eval", new evalBI());
+        putBI("exists", new BuiltInsForExistenceHandling.existsBI());
+        putBI("first", new firstBI());
+        putBI("float", new floatBI());
+        putBI("floor", new floorBI());
+        putBI("chunk", new chunkBI());
+        putBI("counter", new BuiltInsForLoopVariables.counterBI());
+        putBI("item_cycle", "itemCycle", new BuiltInsForLoopVariables.item_cycleBI());
+        putBI("has_api", "hasApi", new BuiltInsForMultipleTypes.has_apiBI());
+        putBI("has_content", "hasContent", new BuiltInsForExistenceHandling.has_contentBI());
+        putBI("has_next", "hasNext", new BuiltInsForLoopVariables.has_nextBI());
+        putBI("html", new BuiltInsForStringsEncoding.htmlBI());
+        putBI("if_exists", "ifExists", new BuiltInsForExistenceHandling.if_existsBI());
+        putBI("index", new BuiltInsForLoopVariables.indexBI());
+        putBI("index_of", "indexOf", new BuiltInsForStringsBasic.index_ofBI(false));
+        putBI("int", new intBI());
+        putBI("interpret", new BuiltInsForStringsMisc.interpretBI());
+        putBI("is_boolean", "isBoolean", new BuiltInsForMultipleTypes.is_booleanBI());
+        putBI("is_collection", "isCollection", new BuiltInsForMultipleTypes.is_collectionBI());
+        putBI("is_collection_ex", "isCollectionEx", new BuiltInsForMultipleTypes.is_collection_exBI());
+        is_dateLikeBI bi = new BuiltInsForMultipleTypes.is_dateLikeBI();
+        putBI("is_date", "isDate", bi);  // misnomer
+        putBI("is_date_like", "isDateLike", bi);
+        putBI("is_date_only", "isDateOnly", new BuiltInsForMultipleTypes.is_dateOfTypeBI(TemplateDateModel.DATE));
+        putBI("is_even_item", "isEvenItem", new BuiltInsForLoopVariables.is_even_itemBI());
+        putBI("is_first", "isFirst", new BuiltInsForLoopVariables.is_firstBI());
+        putBI("is_last", "isLast", new BuiltInsForLoopVariables.is_lastBI());
+        putBI("is_unknown_date_like", "isUnknownDateLike", new BuiltInsForMultipleTypes.is_dateOfTypeBI(TemplateDateModel.UNKNOWN));
+        putBI("is_datetime", "isDatetime", new BuiltInsForMultipleTypes.is_dateOfTypeBI(TemplateDateModel.DATETIME));
+        putBI("is_directive", "isDirective", new BuiltInsForMultipleTypes.is_directiveBI());
+        putBI("is_enumerable", "isEnumerable", new BuiltInsForMultipleTypes.is_enumerableBI());
+        putBI("is_hash_ex", "isHashEx", new BuiltInsForMultipleTypes.is_hash_exBI());
+        putBI("is_hash", "isHash", new BuiltInsForMultipleTypes.is_hashBI());
+        putBI("is_infinite", "isInfinite", new is_infiniteBI());
+        putBI("is_indexable", "isIndexable", new BuiltInsForMultipleTypes.is_indexableBI());
+        putBI("is_macro", "isMacro", new BuiltInsForMultipleTypes.is_macroBI());
+        putBI("is_markup_output", "isMarkupOutput", new BuiltInsForMultipleTypes.is_markup_outputBI());
+        putBI("is_method", "isMethod", new BuiltInsForMultipleTypes.is_methodBI());
+        putBI("is_nan", "isNan", new is_nanBI());
+        putBI("is_node", "isNode", new BuiltInsForMultipleTypes.is_nodeBI());
+        putBI("is_number", "isNumber", new BuiltInsForMultipleTypes.is_numberBI());
+        putBI("is_odd_item", "isOddItem", new BuiltInsForLoopVariables.is_odd_itemBI());
+        putBI("is_sequence", "isSequence", new BuiltInsForMultipleTypes.is_sequenceBI());
+        putBI("is_string", "isString", new BuiltInsForMultipleTypes.is_stringBI());
+        putBI("is_time", "isTime", new BuiltInsForMultipleTypes.is_dateOfTypeBI(TemplateDateModel.TIME));
+        putBI("is_transform", "isTransform", new BuiltInsForMultipleTypes.is_transformBI());
+        
+        putBI("iso_utc", "isoUtc", new iso_utc_or_local_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_SECONDS, /* useUTC = */ true));
+        putBI("iso_utc_fz", "isoUtcFZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.TRUE, _DateUtil.ACCURACY_SECONDS, /* useUTC = */ true));
+        putBI("iso_utc_nz", "isoUtcNZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_SECONDS, /* useUTC = */ true));
+        
+        putBI("iso_utc_ms", "isoUtcMs", new iso_utc_or_local_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_MILLISECONDS, /* useUTC = */ true));
+        putBI("iso_utc_ms_nz", "isoUtcMsNZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MILLISECONDS, /* useUTC = */ true));
+        
+        putBI("iso_utc_m", "isoUtcM", new iso_utc_or_local_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_MINUTES, /* useUTC = */ true));
+        putBI("iso_utc_m_nz", "isoUtcMNZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MINUTES, /* useUTC = */ true));
+        
+        putBI("iso_utc_h", "isoUtcH", new iso_utc_or_local_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_HOURS, /* useUTC = */ true));
+        putBI("iso_utc_h_nz", "isoUtcHNZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_HOURS, /* useUTC = */ true));
+        
+        putBI("iso_local", "isoLocal", new iso_utc_or_local_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_SECONDS, /* useUTC = */ false));
+        putBI("iso_local_nz", "isoLocalNZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_SECONDS, /* useUTC = */ false));
+        
+        putBI("iso_local_ms", "isoLocalMs", new iso_utc_or_local_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_MILLISECONDS, /* useUTC = */ false));
+        putBI("iso_local_ms_nz", "isoLocalMsNZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MILLISECONDS, /* useUTC = */ false));
+        
+        putBI("iso_local_m", "isoLocalM", new iso_utc_or_local_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_MINUTES, /* useUTC = */ false));
+        putBI("iso_local_m_nz", "isoLocalMNZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MINUTES, /* useUTC = */ false));
+        
+        putBI("iso_local_h", "isoLocalH", new iso_utc_or_local_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_HOURS, /* useUTC = */ false));
+        putBI("iso_local_h_nz", "isoLocalHNZ", new iso_utc_or_local_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_HOURS, /* useUTC = */ false));
+        
+        putBI("iso", new iso_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_SECONDS));
+        putBI("iso_nz", "isoNZ", new iso_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_SECONDS));
+        
+        putBI("iso_ms", "isoMs", new iso_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_MILLISECONDS));
+        putBI("iso_ms_nz", "isoMsNZ", new iso_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MILLISECONDS));
+        
+        putBI("iso_m", "isoM", new iso_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_MINUTES));
+        putBI("iso_m_nz", "isoMNZ", new iso_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_MINUTES));
+        
+        putBI("iso_h", "isoH", new iso_BI(
+                /* showOffset = */ null, _DateUtil.ACCURACY_HOURS));
+        putBI("iso_h_nz", "isoHNZ", new iso_BI(
+                /* showOffset = */ Boolean.FALSE, _DateUtil.ACCURACY_HOURS));
+        
+        putBI("j_string", "jString", new BuiltInsForStringsEncoding.j_stringBI());
+        putBI("join", new BuiltInsForSequences.joinBI());
+        putBI("js_string", "jsString", new BuiltInsForStringsEncoding.js_stringBI());
+        putBI("json_string", "jsonString", new BuiltInsForStringsEncoding.json_stringBI());
+        putBI("keep_after", "keepAfter", new BuiltInsForStringsBasic.keep_afterBI());
+        putBI("keep_before", "keepBefore", new BuiltInsForStringsBasic.keep_beforeBI());
+        putBI("keep_after_last", "keepAfterLast", new BuiltInsForStringsBasic.keep_after_lastBI());
+        putBI("keep_before_last", "keepBeforeLast", new BuiltInsForStringsBasic.keep_before_lastBI());
+        putBI("keys", new BuiltInsForHashes.keysBI());
+        putBI("last_index_of", "lastIndexOf", new BuiltInsForStringsBasic.index_ofBI(true));
+        putBI("last", new lastBI());
+        putBI("left_pad", "leftPad", new BuiltInsForStringsBasic.padBI(true));
+        putBI("length", new BuiltInsForStringsBasic.lengthBI());
+        putBI("long", new longBI());
+        putBI("lower_abc", "lowerAbc", new BuiltInsForNumbers.lower_abcBI());
+        putBI("lower_case", "lowerCase", new BuiltInsForStringsBasic.lower_caseBI());
+        putBI("namespace", new BuiltInsForMultipleTypes.namespaceBI());
+        putBI("new", new BuiltInsForStringsMisc.newBI());
+        putBI("markup_string", "markupString", new markup_stringBI());
+        putBI("node_name", "nodeName", new node_nameBI());
+        putBI("node_namespace", "nodeNamespace", new node_namespaceBI());
+        putBI("node_type", "nodeType", new node_typeBI());
+        putBI("no_esc", "noEsc", new no_escBI());
+        putBI("number", new BuiltInsForStringsMisc.numberBI());
+        putBI("number_to_date", "numberToDate", new number_to_dateBI(TemplateDateModel.DATE));
+        putBI("number_to_time", "numberToTime", new number_to_dateBI(TemplateDateModel.TIME));
+        putBI("number_to_datetime", "numberToDatetime", new number_to_dateBI(TemplateDateModel.DATETIME));
+        putBI("parent", new parentBI());
+        putBI("previous_sibling", "previousSibling", new previousSiblingBI());
+        putBI("next_sibling", "nextSibling", new nextSiblingBI());
+        putBI("item_parity", "itemParity", new BuiltInsForLoopVariables.item_parityBI());
+        putBI("item_parity_cap", "itemParityCap", new BuiltInsForLoopVariables.item_parity_capBI());
+        putBI("reverse", new reverseBI());
+        putBI("right_pad", "rightPad", new BuiltInsForStringsBasic.padBI(false));
+        putBI("root", new rootBI());
+        putBI("round", new roundBI());
+        putBI("remove_ending", "removeEnding", new BuiltInsForStringsBasic.remove_endingBI());
+        putBI("remove_beginning", "removeBeginning", new BuiltInsForStringsBasic.remove_beginningBI());
+        putBI("rtf", new BuiltInsForStringsEncoding.rtfBI());
+        putBI("seq_contains", "seqContains", new seq_containsBI());
+        putBI("seq_index_of", "seqIndexOf", new seq_index_ofBI(1));
+        putBI("seq_last_index_of", "seqLastIndexOf", new seq_index_ofBI(-1));
+        putBI("short", new shortBI());
+        putBI("size", new BuiltInsForMultipleTypes.sizeBI());
+        putBI("sort_by", "sortBy", new sort_byBI());
+        putBI("sort", new sortBI());
+        putBI("split", new BuiltInsForStringsBasic.split_BI());
+        putBI("switch", new BuiltInsWithParseTimeParameters.switch_BI());
+        putBI("starts_with", "startsWith", new BuiltInsForStringsBasic.starts_withBI());
+        putBI("string", new BuiltInsForMultipleTypes.stringBI());
+        putBI("substring", new BuiltInsForStringsBasic.substringBI());
+        putBI("then", new BuiltInsWithParseTimeParameters.then_BI());
+        putBI("time", new BuiltInsForMultipleTypes.dateBI(TemplateDateModel.TIME));
+        putBI("time_if_unknown", "timeIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.TIME));
+        putBI("trim", new BuiltInsForStringsBasic.trimBI());
+        putBI("uncap_first", "uncapFirst", new BuiltInsForStringsBasic.uncap_firstBI());
+        putBI("upper_abc", "upperAbc", new BuiltInsForNumbers.upper_abcBI());
+        putBI("upper_case", "upperCase", new BuiltInsForStringsBasic.upper_caseBI());
+        putBI("url", new BuiltInsForStringsEncoding.urlBI());
+        putBI("url_path", "urlPath", new BuiltInsForStringsEncoding.urlPathBI());
+        putBI("values", new BuiltInsForHashes.valuesBI());
+        putBI("web_safe", "webSafe", BUILT_INS_BY_NAME.get("html"));  // deprecated; use ?html instead
+        putBI("word_list", "wordList", new BuiltInsForStringsBasic.word_listBI());
+        putBI("xhtml", new BuiltInsForStringsEncoding.xhtmlBI());
+        putBI("xml", new BuiltInsForStringsEncoding.xmlBI());
+        putBI("matches", new BuiltInsForStringsRegexp.matchesBI());
+        putBI("groups", new BuiltInsForStringsRegexp.groupsBI());
+        putBI("replace", new BuiltInsForStringsRegexp.replace_reBI());
+
+        
+        if (NUMBER_OF_BIS < BUILT_INS_BY_NAME.size()) {
+            throw new AssertionError("Update NUMBER_OF_BIS! Should be: " + BUILT_INS_BY_NAME.size());
+        }
+    }
+    
+    private static void putBI(String name, ASTExpBuiltIn bi) {
+        BUILT_INS_BY_NAME.put(name, bi);
+        SNAKE_CASE_NAMES.add(name);
+        CAMEL_CASE_NAMES.add(name);
+    }
+
+    private static void putBI(String nameSnakeCase, String nameCamelCase, ASTExpBuiltIn bi) {
+        BUILT_INS_BY_NAME.put(nameSnakeCase, bi);
+        BUILT_INS_BY_NAME.put(nameCamelCase, bi);
+        SNAKE_CASE_NAMES.add(nameSnakeCase);
+        CAMEL_CASE_NAMES.add(nameCamelCase);
+    }
+    
+    /**
+     * @param target
+     *            Left-hand-operand expression
+     * @param keyTk
+     *            Built-in name token
+     */
+    static ASTExpBuiltIn newBuiltIn(int incompatibleImprovements, ASTExpression target, Token keyTk,
+            FMParserTokenManager tokenManager) throws ParseException {
+        String key = keyTk.image;
+        ASTExpBuiltIn bi = BUILT_INS_BY_NAME.get(key);
+        if (bi == null) {
+            StringBuilder buf = new StringBuilder("Unknown built-in: ").append(_StringUtil.jQuote(key)).append(". ");
+            
+            buf.append(
+                    "Help (latest version): http://freemarker.org/docs/ref_builtins.html; "
+                    + "you're using FreeMarker ").append(Configuration.getVersion()).append(".\n" 
+                    + "The alphabetical list of built-ins:");
+            List<String> names = new ArrayList<>(BUILT_INS_BY_NAME.keySet().size());
+            names.addAll(BUILT_INS_BY_NAME.keySet());
+            Collections.sort(names);
+            char lastLetter = 0;
+            
+            int shownNamingConvention;
+            {
+                int namingConvention = tokenManager.namingConvention;
+                shownNamingConvention = namingConvention != ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION
+                        ? namingConvention : ParsingConfiguration.LEGACY_NAMING_CONVENTION /* [2.4] CAMEL_CASE */;
+            }
+            
+            boolean first = true;
+            for (String correctName : names) {
+                int correctNameNamingConvetion = _StringUtil.getIdentifierNamingConvention(correctName);
+                if (shownNamingConvention == ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION
+                        ? correctNameNamingConvetion != ParsingConfiguration.LEGACY_NAMING_CONVENTION
+                        : correctNameNamingConvetion != ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION) {
+                    if (first) {
+                        first = false;
+                    } else {
+                        buf.append(", ");
+                    }
+
+                    char firstChar = correctName.charAt(0);
+                    if (firstChar != lastLetter) {
+                        lastLetter = firstChar;
+                        buf.append('\n');
+                    }
+                    buf.append(correctName);
+                }
+            }
+                
+            throw new ParseException(buf.toString(), null, keyTk);
+        }
+        
+        try {
+            bi = (ASTExpBuiltIn) bi.clone();
+        } catch (CloneNotSupportedException e) {
+            throw new InternalError();
+        }
+        bi.key = key;
+        bi.target = target;
+        return bi;
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return target.getCanonicalForm() + "?" + key;
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "?" + key;
+    }
+
+    @Override
+    boolean isLiteral() {
+        return false; // be on the safe side.
+    }
+    
+    protected final void checkMethodArgCount(List args, int expectedCnt) throws TemplateModelException {
+        checkMethodArgCount(args.size(), expectedCnt);
+    }
+    
+    protected final void checkMethodArgCount(int argCnt, int expectedCnt) throws TemplateModelException {
+        if (argCnt != expectedCnt) {
+            throw MessageUtil.newArgCntError("?" + key, argCnt, expectedCnt);
+        }
+    }
+
+    protected final void checkMethodArgCount(List args, int minCnt, int maxCnt) throws TemplateModelException {
+        checkMethodArgCount(args.size(), minCnt, maxCnt);
+    }
+    
+    protected final void checkMethodArgCount(int argCnt, int minCnt, int maxCnt) throws TemplateModelException {
+        if (argCnt < minCnt || argCnt > maxCnt) {
+            throw MessageUtil.newArgCntError("?" + key, argCnt, minCnt, maxCnt);
+        }
+    }
+
+    /**
+     * Same as {@link #getStringMethodArg}, but checks if {@code args} is big enough, and returns {@code null} if it
+     * isn't.
+     */
+    protected final String getOptStringMethodArg(List args, int argIdx)
+            throws TemplateModelException {
+        return args.size() > argIdx ? getStringMethodArg(args, argIdx) : null;
+    }
+    
+    /**
+     * Gets a method argument and checks if it's a string; it does NOT check if {@code args} is big enough.
+     */
+    protected final String getStringMethodArg(List args, int argIdx)
+            throws TemplateModelException {
+        TemplateModel arg = (TemplateModel) args.get(argIdx);
+        if (!(arg instanceof TemplateScalarModel)) {
+            throw MessageUtil.newMethodArgMustBeStringException("?" + key, argIdx, arg);
+        } else {
+            return _EvalUtil.modelToString((TemplateScalarModel) arg, null, null);
+        }
+    }
+
+    /**
+     * Gets a method argument and checks if it's a number; it does NOT check if {@code args} is big enough.
+     */
+    protected final Number getNumberMethodArg(List args, int argIdx)
+            throws TemplateModelException {
+        TemplateModel arg = (TemplateModel) args.get(argIdx);
+        if (!(arg instanceof TemplateNumberModel)) {
+            throw MessageUtil.newMethodArgMustBeNumberException("?" + key, argIdx, arg);
+        } else {
+            return _EvalUtil.modelToNumber((TemplateNumberModel) arg, null);
+        }
+    }
+    
+    protected final TemplateModelException newMethodArgInvalidValueException(int argIdx, Object[] details) {
+        return MessageUtil.newMethodArgInvalidValueException("?" + key, argIdx, details);
+    }
+
+    protected final TemplateModelException newMethodArgsInvalidValueException(Object[] details) {
+        return MessageUtil.newMethodArgsInvalidValueException("?" + key, details);
+    }
+    
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	try {
+	    	ASTExpBuiltIn clone = (ASTExpBuiltIn) clone();
+	    	clone.target = target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState);
+	    	return clone;
+        } catch (CloneNotSupportedException e) {
+            throw new RuntimeException("Internal error: " + e);
+        }
+    }
+
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        switch (idx) {
+        case 0: return target;
+        case 1: return key;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        switch (idx) {
+        case 0: return ParameterRole.LEFT_HAND_OPERAND;
+        case 1: return ParameterRole.RIGHT_HAND_OPERAND;
+        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/ASTExpBuiltInVariable.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java
new file mode 100644
index 0000000..ece2099
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java
@@ -0,0 +1,298 @@
+/*
+ * 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.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Date;
+
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.impl.SimpleDate;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST expression node: {@code .name}
+ */
+final class ASTExpBuiltInVariable extends ASTExpression {
+
+    static final String TEMPLATE_NAME_CC = "templateName";
+    static final String TEMPLATE_NAME = "template_name";
+    static final String MAIN_TEMPLATE_NAME_CC = "mainTemplateName";
+    static final String MAIN_TEMPLATE_NAME = "main_template_name";
+    static final String CURRENT_TEMPLATE_NAME_CC = "currentTemplateName";
+    static final String CURRENT_TEMPLATE_NAME = "current_template_name";
+    static final String NAMESPACE = "namespace";
+    static final String MAIN = "main";
+    static final String GLOBALS = "globals";
+    static final String LOCALS = "locals";
+    static final String DATA_MODEL_CC = "dataModel";
+    static final String DATA_MODEL = "data_model";
+    static final String LANG = "lang";
+    static final String LOCALE = "locale";
+    static final String LOCALE_OBJECT_CC = "localeObject";
+    static final String LOCALE_OBJECT = "locale_object";
+    static final String CURRENT_NODE_CC = "currentNode";
+    static final String CURRENT_NODE = "current_node";
+    static final String NODE = "node";
+    static final String PASS = "pass";
+    static final String VARS = "vars";
+    static final String VERSION = "version";
+    static final String INCOMPATIBLE_IMPROVEMENTS_CC = "incompatibleImprovements";
+    static final String INCOMPATIBLE_IMPROVEMENTS = "incompatible_improvements";
+    static final String ERROR = "error";
+    static final String OUTPUT_ENCODING_CC = "outputEncoding";
+    static final String OUTPUT_ENCODING = "output_encoding";
+    static final String OUTPUT_FORMAT_CC = "outputFormat";
+    static final String OUTPUT_FORMAT = "output_format";
+    static final String AUTO_ESC_CC = "autoEsc";
+    static final String AUTO_ESC = "auto_esc";
+    static final String URL_ESCAPING_CHARSET_CC = "urlEscapingCharset";
+    static final String URL_ESCAPING_CHARSET = "url_escaping_charset";
+    static final String NOW = "now";
+    
+    static final String[] SPEC_VAR_NAMES = new String[] {
+        AUTO_ESC_CC,
+        AUTO_ESC,
+        CURRENT_NODE_CC,
+        CURRENT_TEMPLATE_NAME_CC,
+        CURRENT_NODE,
+        CURRENT_TEMPLATE_NAME,
+        DATA_MODEL_CC,
+        DATA_MODEL,
+        ERROR,
+        GLOBALS,
+        INCOMPATIBLE_IMPROVEMENTS_CC,
+        INCOMPATIBLE_IMPROVEMENTS,
+        LANG,
+        LOCALE,
+        LOCALE_OBJECT_CC,
+        LOCALE_OBJECT,
+        LOCALS,
+        MAIN,
+        MAIN_TEMPLATE_NAME_CC,
+        MAIN_TEMPLATE_NAME,
+        NAMESPACE,
+        NODE,
+        NOW,
+        OUTPUT_ENCODING_CC,
+        OUTPUT_FORMAT_CC,
+        OUTPUT_ENCODING,
+        OUTPUT_FORMAT,
+        PASS,
+        TEMPLATE_NAME_CC,
+        TEMPLATE_NAME,
+        URL_ESCAPING_CHARSET_CC,
+        URL_ESCAPING_CHARSET,
+        VARS,
+        VERSION
+    };
+
+    private final String name;
+    private final TemplateModel parseTimeValue;
+
+    ASTExpBuiltInVariable(Token nameTk, FMParserTokenManager tokenManager, TemplateModel parseTimeValue)
+            throws ParseException {
+        String name = nameTk.image;
+        this.parseTimeValue = parseTimeValue;
+        if (Arrays.binarySearch(SPEC_VAR_NAMES, name) < 0) {
+            StringBuilder sb = new StringBuilder();
+            sb.append("Unknown special variable name: ");
+            sb.append(_StringUtil.jQuote(name)).append(".");
+            
+            int shownNamingConvention;
+            {
+                int namingConvention = tokenManager.namingConvention;
+                shownNamingConvention = namingConvention != ParsingConfiguration.AUTO_DETECT_NAMING_CONVENTION
+                        ? namingConvention : ParsingConfiguration.LEGACY_NAMING_CONVENTION /* [2.4] CAMEL_CASE */;
+            }
+            
+            {
+                String correctName;
+                if (name.equals("auto_escape") || name.equals("auto_escaping") || name.equals("autoesc")) {
+                    correctName = "auto_esc";
+                } else if (name.equals("autoEscape") || name.equals("autoEscaping")) {
+                    correctName = "autoEsc";
+                } else {
+                    correctName = null;
+                }
+                if (correctName != null) {
+                    sb.append(" You may meant: ");
+                    sb.append(_StringUtil.jQuote(correctName)).append(".");
+                }
+            }
+            
+            sb.append("\nThe allowed special variable names are: ");
+            boolean first = true;
+            for (final String correctName : SPEC_VAR_NAMES) {
+                int correctNameNamingConvention = _StringUtil.getIdentifierNamingConvention(correctName);
+                if (shownNamingConvention == ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION
+                        ? correctNameNamingConvention != ParsingConfiguration.LEGACY_NAMING_CONVENTION
+                        : correctNameNamingConvention != ParsingConfiguration.CAMEL_CASE_NAMING_CONVENTION) {
+                    if (first) {
+                        first = false;
+                    } else {
+                        sb.append(", ");
+                    }
+                    sb.append(correctName);
+                }
+            }
+            throw new ParseException(sb.toString(), null, nameTk);
+        }
+        
+        this.name = name.intern();
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        if (parseTimeValue != null) {
+            return parseTimeValue;
+        }
+        if (name == NAMESPACE) {
+            return env.getCurrentNamespace();
+        }
+        if (name == MAIN) {
+            return env.getMainNamespace();
+        }
+        if (name == GLOBALS) {
+            return env.getGlobalVariables();
+        }
+        if (name == LOCALS) {
+            ASTDirMacro.Context ctx = env.getCurrentMacroContext();
+            return ctx == null ? null : ctx.getLocals();
+        }
+        if (name == DATA_MODEL || name == DATA_MODEL_CC) {
+            return env.getDataModel();
+        }
+        if (name == VARS) {
+            return new VarsHash(env);
+        }
+        if (name == LOCALE) {
+            return new SimpleScalar(env.getLocale().toString());
+        }
+        if (name == LOCALE_OBJECT || name == LOCALE_OBJECT_CC) {
+            return env.getObjectWrapper().wrap(env.getLocale());
+        }
+        if (name == LANG) {
+            return new SimpleScalar(env.getLocale().getLanguage());
+        }
+        if (name == CURRENT_NODE || name == NODE || name == CURRENT_NODE_CC) {
+            return env.getCurrentVisitorNode();
+        }
+        if (name == MAIN_TEMPLATE_NAME || name == MAIN_TEMPLATE_NAME_CC) {
+            return SimpleScalar.newInstanceOrNull(env.getMainTemplate().getLookupName());
+        }
+        // [FM3] Some of these two should be removed.
+        if (name == CURRENT_TEMPLATE_NAME || name == CURRENT_TEMPLATE_NAME_CC
+                || name == TEMPLATE_NAME || name == TEMPLATE_NAME_CC) {
+            return SimpleScalar.newInstanceOrNull(env.getCurrentTemplate().getLookupName());
+        }
+        if (name == PASS) {
+            return ASTDirMacro.DO_NOTHING_MACRO;
+        }
+        if (name == OUTPUT_ENCODING || name == OUTPUT_ENCODING_CC) {
+            Charset encoding = env.getOutputEncoding();
+            return encoding != null ? new SimpleScalar(encoding.name()) : null;
+        }
+        if (name == URL_ESCAPING_CHARSET || name == URL_ESCAPING_CHARSET_CC) {
+            Charset charset = env.getURLEscapingCharset();
+            return charset != null ? new SimpleScalar(charset.name()) : null;
+        }
+        if (name == ERROR) {
+            return new SimpleScalar(env.getCurrentRecoveredErrorMessage());
+        }
+        if (name == NOW) {
+            return new SimpleDate(new Date(), TemplateDateModel.DATETIME);
+        }
+        if (name == VERSION) {
+            return new SimpleScalar(Configuration.getVersion().toString());
+        }
+        if (name == INCOMPATIBLE_IMPROVEMENTS || name == INCOMPATIBLE_IMPROVEMENTS_CC) {
+            return new SimpleScalar(env.getConfiguration().getIncompatibleImprovements().toString());
+        }
+        
+        throw new _MiscTemplateException(this,
+                "Invalid special variable: ", name);
+    }
+
+    @Override
+    public String toString() {
+        return "." + name;
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return "." + name;
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return getCanonicalForm();
+    }
+
+    @Override
+    boolean isLiteral() {
+        return false;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        return this;
+    }
+
+    static class VarsHash implements TemplateHashModel {
+        
+        Environment env;
+        
+        VarsHash(Environment env) {
+            this.env = env;
+        }
+        
+        @Override
+        public TemplateModel get(String key) throws TemplateModelException {
+            return env.getVariable(key);
+        }
+        
+        @Override
+        public boolean isEmpty() {
+            return false;
+        }
+    }
+    
+    @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/ASTExpComparison.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpComparison.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpComparison.java
new file mode 100644
index 0000000..4e3559f
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpComparison.java
@@ -0,0 +1,104 @@
+/*
+ * 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.util.BugException;
+
+/**
+ * AST expression node: Comparison operators, like {@code ==}, {@code !=}, {@code <}, etc.
+ */
+final class ASTExpComparison extends ASTExpBoolean {
+
+    private final ASTExpression left;
+    private final ASTExpression right;
+    private final int operation;
+    private final String opString;
+
+    ASTExpComparison(ASTExpression left, ASTExpression right, String opString) {
+        this.left = left;
+        this.right = right;
+        opString = opString.intern();
+        this.opString = opString;
+        if (opString == "==" || opString == "=") {
+            operation = _EvalUtil.CMP_OP_EQUALS;
+        } else if (opString == "!=") {
+            operation = _EvalUtil.CMP_OP_NOT_EQUALS;
+        } else if (opString == "gt" || opString == "\\gt" || opString == ">" || opString == "&gt;") {
+            operation = _EvalUtil.CMP_OP_GREATER_THAN;
+        } else if (opString == "gte" || opString == "\\gte" || opString == ">=" || opString == "&gt;=") {
+            operation = _EvalUtil.CMP_OP_GREATER_THAN_EQUALS;
+        } else if (opString == "lt" || opString == "\\lt" || opString == "<" || opString == "&lt;") {
+            operation = _EvalUtil.CMP_OP_LESS_THAN;
+        } else if (opString == "lte" || opString == "\\lte" || opString == "<=" || opString == "&lt;=") {
+            operation = _EvalUtil.CMP_OP_LESS_THAN_EQUALS;
+        } else {
+            throw new BugException("Unknown comparison operator " + opString);
+        }
+    }
+
+    /*
+     * WARNING! This algorithm is duplicated in SequenceBuiltins.modelsEqual.
+     * Thus, if you update this method, then you have to update that too!
+     */
+    @Override
+    boolean evalToBoolean(Environment env) throws TemplateException {
+        return _EvalUtil.compare(left, operation, opString, right, this, env);
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return left.getCanonicalForm() + ' ' + opString + ' ' + right.getCanonicalForm();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return opString;
+    }
+
+    @Override
+    boolean isLiteral() {
+        return constantValue != null || (left.isLiteral() && right.isLiteral());
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpComparison(
+    	        left.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        right.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        opString);
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        return idx == 0 ? left : right;
+    }
+
+    @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/ASTExpDefault.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDefault.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDefault.java
new file mode 100644
index 0000000..b891374
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDefault.java
@@ -0,0 +1,142 @@
+/*
+ * 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.Constants;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+
+/** {@code exp!defExp}, {@code (exp)!defExp} and the same two with {@code (exp)!}. */
+class ASTExpDefault extends ASTExpression {
+	
+	static private class EmptyStringAndSequence
+	  implements TemplateScalarModel, TemplateSequenceModel, TemplateHashModelEx {
+		@Override
+        public String getAsString() {
+			return "";
+		}
+		@Override
+        public TemplateModel get(int i) {
+			return null;
+		}
+		@Override
+        public TemplateModel get(String s) {
+			return null;
+		}
+		@Override
+        public int size() {
+			return 0;
+		}
+		@Override
+        public boolean isEmpty() {
+			return true;
+		}
+		@Override
+        public TemplateCollectionModel keys() {
+			return Constants.EMPTY_COLLECTION;
+		}
+		@Override
+        public TemplateCollectionModel values() {
+			return Constants.EMPTY_COLLECTION;
+		}
+		
+	}
+	
+	static final TemplateModel EMPTY_STRING_AND_SEQUENCE = new EmptyStringAndSequence();
+	
+	private final ASTExpression lho, rho;
+	
+	ASTExpDefault(ASTExpression lho, ASTExpression rho) {
+		this.lho = lho;
+		this.rho = rho;
+	}
+
+	@Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+		TemplateModel left;
+		if (lho instanceof ASTExpParenthesis) {
+            boolean lastFIRE = env.setFastInvalidReferenceExceptions(true);
+	        try {
+                left = lho.eval(env);
+	        } catch (InvalidReferenceException ire) {
+	            left = null;
+            } finally {
+                env.setFastInvalidReferenceExceptions(lastFIRE);
+	        }
+		} else {
+            left = lho.eval(env);
+		}
+		
+		if (left != null) return left;
+		else if (rho == null) return EMPTY_STRING_AND_SEQUENCE;
+		else return rho.eval(env);
+	}
+
+	@Override
+    boolean isLiteral() {
+		return false;
+	}
+
+	@Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+        return new ASTExpDefault(
+                lho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+                rho != null
+                        ? rho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState)
+                        : null);
+	}
+
+	@Override
+    public String getCanonicalForm() {
+		if (rho == null) {
+			return lho.getCanonicalForm() + '!';
+		}
+		return lho.getCanonicalForm() + '!' + rho.getCanonicalForm();
+	}
+	
+	@Override
+    String getNodeTypeSymbol() {
+        return "...!...";
+    }
+    
+    @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/ASTExpDot.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDot.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDot.java
new file mode 100644
index 0000000..1e6a742
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDot.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.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * AST expression node: {@code .} operator.
+ */
+final class ASTExpDot extends ASTExpression {
+    private final ASTExpression target;
+    private final String key;
+
+    ASTExpDot(ASTExpression target, String key) {
+        this.target = target;
+        this.key = key;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        TemplateModel leftModel = target.eval(env);
+        if (leftModel instanceof TemplateHashModel) {
+            return ((TemplateHashModel) leftModel).get(key);
+        }
+        throw new NonHashException(target, leftModel, env);
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return target.getCanonicalForm() + getNodeTypeSymbol() + _StringUtil.toFTLIdentifierReferenceAfterDot(key);
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return ".";
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return target.isLiteral();
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpDot(
+    	        target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        key);
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        return idx == 0 ? target : key;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.forBinaryOperatorOperand(idx);
+    }
+    
+    String getRHO() {
+        return key;
+    }
+
+    boolean onlyHasIdentifiers() {
+        return (target instanceof ASTExpVariable) || ((target instanceof ASTExpDot) && ((ASTExpDot) target).onlyHasIdentifiers());
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDynamicKeyName.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDynamicKeyName.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDynamicKeyName.java
new file mode 100644
index 0000000..b904ce4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpDynamicKeyName.java
@@ -0,0 +1,284 @@
+/*
+ * 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.Constants;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+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.SimpleScalar;
+
+/**
+ * AST expression node: {@code target[keyExpression]}, where, in FM 2.3, {@code keyExpression} can be string, a number
+ * or a range, and {@code target} can be a hash or a sequence.
+ */
+final class ASTExpDynamicKeyName extends ASTExpression {
+
+    private final ASTExpression keyExpression;
+    private final ASTExpression target;
+
+    ASTExpDynamicKeyName(ASTExpression target, ASTExpression keyExpression) {
+        this.target = target; 
+        this.keyExpression = keyExpression;
+    }
+
+    @Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        TemplateModel targetModel = target.eval(env);
+        target.assertNonNull(targetModel, env);
+        
+        TemplateModel keyModel = keyExpression.eval(env);
+        keyExpression.assertNonNull(keyModel, env);
+        if (keyModel instanceof TemplateNumberModel) {
+            int index = keyExpression.modelToNumber(keyModel, env).intValue();
+            return dealWithNumericalKey(targetModel, index, env);
+        }
+        if (keyModel instanceof TemplateScalarModel) {
+            String key = _EvalUtil.modelToString((TemplateScalarModel) keyModel, keyExpression, env);
+            return dealWithStringKey(targetModel, key, env);
+        }
+        if (keyModel instanceof RangeModel) {
+            return dealWithRangeKey(targetModel, (RangeModel) keyModel, env);
+        }
+        throw new UnexpectedTypeException(keyExpression, keyModel, "number, range, or string",
+                new Class[] { TemplateNumberModel.class, TemplateScalarModel.class, ASTExpRange.class }, env);
+    }
+
+    static private Class[] NUMERICAL_KEY_LHO_EXPECTED_TYPES;
+    static {
+        NUMERICAL_KEY_LHO_EXPECTED_TYPES = new Class[1 + NonStringException.STRING_COERCABLE_TYPES.length];
+        NUMERICAL_KEY_LHO_EXPECTED_TYPES[0] = TemplateSequenceModel.class;
+        for (int i = 0; i < NonStringException.STRING_COERCABLE_TYPES.length; i++) {
+            NUMERICAL_KEY_LHO_EXPECTED_TYPES[i + 1] = NonStringException.STRING_COERCABLE_TYPES[i];
+        }
+    }
+    
+    private TemplateModel dealWithNumericalKey(TemplateModel targetModel, 
+                                               int index, 
+                                               Environment env)
+        throws TemplateException {
+        if (targetModel instanceof TemplateSequenceModel) {
+            TemplateSequenceModel tsm = (TemplateSequenceModel) targetModel;
+            int size;
+            try {
+                size = tsm.size();
+            } catch (Exception e) {
+                size = Integer.MAX_VALUE;
+            }
+            return index < size ? tsm.get(index) : null;
+        } 
+        
+        try {
+            String s = target.evalAndCoerceToPlainText(env);
+            try {
+                return new SimpleScalar(s.substring(index, index + 1));
+            } catch (IndexOutOfBoundsException e) {
+                if (index < 0) {
+                    throw new _MiscTemplateException("Negative index not allowed: ", Integer.valueOf(index));
+                }
+                if (index >= s.length()) {
+                    throw new _MiscTemplateException(
+                            "String index out of range: The index was ", Integer.valueOf(index),
+                            " (0-based), but the length of the string is only ", Integer.valueOf(s.length()) , ".");
+                }
+                throw new RuntimeException("Can't explain exception", e);
+            }
+        } catch (NonStringException e) {
+            throw new UnexpectedTypeException(
+                    target, targetModel,
+                    "sequence or " + NonStringException.STRING_COERCABLE_TYPES_DESC,
+                    NUMERICAL_KEY_LHO_EXPECTED_TYPES,
+                    (targetModel instanceof TemplateHashModel
+                            ? "You had a numberical value inside the []. Currently that's only supported for "
+                                    + "sequences (lists) and strings. To get a Map item with a non-string key, "
+                                    + "use myMap?api.get(myKey)."
+                            : null),
+                    env);
+        }
+    }
+
+    private TemplateModel dealWithStringKey(TemplateModel targetModel, String key, Environment env)
+        throws TemplateException {
+        if (targetModel instanceof TemplateHashModel) {
+            return((TemplateHashModel) targetModel).get(key);
+        }
+        throw new NonHashException(target, targetModel, env);
+    }
+
+    private TemplateModel dealWithRangeKey(TemplateModel targetModel, RangeModel range, Environment env)
+    throws TemplateException {
+        final TemplateSequenceModel targetSeq;
+        final String targetStr;
+        if (targetModel instanceof TemplateSequenceModel) {
+            targetSeq = (TemplateSequenceModel) targetModel;
+            targetStr = null;
+        } else {
+            targetSeq = null;
+            try {
+                targetStr = target.evalAndCoerceToPlainText(env);
+            } catch (NonStringException e) {
+                throw new UnexpectedTypeException(
+                        target, target.eval(env),
+                        "sequence or " + NonStringException.STRING_COERCABLE_TYPES_DESC,
+                        NUMERICAL_KEY_LHO_EXPECTED_TYPES, env);
+            }
+        }
+        
+        final int size = range.size();
+        final boolean rightUnbounded = range.isRightUnbounded();
+        final boolean rightAdaptive = range.isRightAdaptive();
+        
+        // Right bounded empty ranges are accepted even if the begin index is out of bounds. That's because a such range
+        // produces an empty sequence, which thus doesn't contain any illegal indexes.
+        if (!rightUnbounded && size == 0) {
+            return emptyResult(targetSeq != null);
+        }
+
+        final int firstIdx = range.getBegining();
+        if (firstIdx < 0) {
+            throw new _MiscTemplateException(keyExpression,
+                    "Negative range start index (", Integer.valueOf(firstIdx),
+                    ") isn't allowed for a range used for slicing.");
+        }
+        
+        final int targetSize = targetStr != null ? targetStr.length() : targetSeq.size();
+        final int step = range.getStep();
+        
+        // Right-adaptive increasing ranges can start 1 after the last element of the target, because they are like
+        // ranges with exclusive end index of at most targetSize. Thence a such range is just an empty list of indexes,
+        // and thus it isn't out-of-bounds.
+        // Right-adaptive decreasing ranges has exclusive end -1, so it can't help on a  to high firstIndex. 
+        // Right-bounded ranges at this point aren't empty, so the right index surely can't reach targetSize. 
+        if (rightAdaptive && step == 1 ? firstIdx > targetSize : firstIdx >= targetSize) {
+            throw new _MiscTemplateException(keyExpression,
+                    "Range start index ", Integer.valueOf(firstIdx), " is out of bounds, because the sliced ",
+                    (targetStr != null ? "string" : "sequence"),
+                    " has only ", Integer.valueOf(targetSize), " ", (targetStr != null ? "character(s)" : "element(s)"),
+                    ". ", "(Note that indices are 0-based).");
+        }
+        
+        final int resultSize;
+        if (!rightUnbounded) {
+            final int lastIdx = firstIdx + (size - 1) * step;
+            if (lastIdx < 0) {
+                if (!rightAdaptive) {
+                    throw new _MiscTemplateException(keyExpression,
+                            "Negative range end index (", Integer.valueOf(lastIdx),
+                            ") isn't allowed for a range used for slicing.");
+                } else {
+                    resultSize = firstIdx + 1;
+                }
+            } else if (lastIdx >= targetSize) {
+                if (!rightAdaptive) {
+                    throw new _MiscTemplateException(keyExpression,
+                            "Range end index ", Integer.valueOf(lastIdx), " is out of bounds, because the sliced ",
+                            (targetStr != null ? "string" : "sequence"),
+                            " has only ", Integer.valueOf(targetSize), " ", (targetStr != null ? "character(s)" : "element(s)"),
+                            ". (Note that indices are 0-based).");
+                } else {
+                    resultSize = Math.abs(targetSize - firstIdx);
+                }
+            } else {
+                resultSize = size;
+            }
+        } else {
+            resultSize = targetSize - firstIdx;
+        }
+        
+        if (resultSize == 0) {
+            return emptyResult(targetSeq != null);
+        }
+        if (targetSeq != null) {
+            NativeSequence resultSeq = new NativeSequence(resultSize);
+            int srcIdx = firstIdx;
+            for (int i = 0; i < resultSize; i++) {
+                resultSeq.add(targetSeq.get(srcIdx));
+                srcIdx += step;
+            }
+            // List items are already wrapped, so the wrapper will be null:
+            return resultSeq;
+        } else {
+            final int exclEndIdx;
+            if (step < 0 && resultSize > 1) {
+                if (!(range.isAffactedByStringSlicingBug() && resultSize == 2)) {
+                    throw new _MiscTemplateException(keyExpression,
+                            "Decreasing ranges aren't allowed for slicing strings (as it would give reversed text). "
+                            + "The index range was: first = ", Integer.valueOf(firstIdx),
+                            ", last = ", Integer.valueOf(firstIdx + (resultSize - 1) * step));
+                } else {
+                    // Emulate the legacy bug, where "foo"[n .. n-1] gives "" instead of an error (if n >= 1).  
+                    // Fix this in FTL [2.4]
+                    exclEndIdx = firstIdx;
+                }
+            } else {
+                exclEndIdx = firstIdx + resultSize;
+            }
+            
+            return new SimpleScalar(targetStr.substring(firstIdx, exclEndIdx));
+        }
+    }
+
+    private TemplateModel emptyResult(boolean seq) {
+        return seq ? Constants.EMPTY_SEQUENCE : TemplateScalarModel.EMPTY_STRING;
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return target.getCanonicalForm() 
+               + "[" 
+               + keyExpression.getCanonicalForm() 
+               + "]";
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "...[...]";
+    }
+    
+    @Override
+    boolean isLiteral() {
+        return constantValue != null || (target.isLiteral() && keyExpression.isLiteral());
+    }
+    
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        return idx == 0 ? target : keyExpression;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return idx == 0 ? ParameterRole.LEFT_HAND_OPERAND : ParameterRole.ENCLOSED_OPERAND;
+    }
+
+    @Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+    	return new ASTExpDynamicKeyName(
+    	        target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	        keyExpression.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpExists.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpExists.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpExists.java
new file mode 100644
index 0000000..72b8182
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpExists.java
@@ -0,0 +1,91 @@
+/*
+ * 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.TemplateModel;
+
+/**
+ * AST expression node: {@code ??} operator.
+ */
+class ASTExpExists extends ASTExpression {
+	
+	protected final ASTExpression exp;
+	
+	ASTExpExists(ASTExpression exp) {
+		this.exp = exp;
+	}
+
+	@Override
+    TemplateModel _eval(Environment env) throws TemplateException {
+        TemplateModel tm;
+	    if (exp instanceof ASTExpParenthesis) {
+            boolean lastFIRE = env.setFastInvalidReferenceExceptions(true);
+            try {
+                tm = exp.eval(env);
+            } catch (InvalidReferenceException ire) {
+                tm = null;
+            } finally {
+                env.setFastInvalidReferenceExceptions(lastFIRE);
+            }
+	    } else {
+            tm = exp.eval(env);
+	    }
+		return tm == null ? TemplateBooleanModel.FALSE : TemplateBooleanModel.TRUE;
+	}
+
+	@Override
+    boolean isLiteral() {
+		return false;
+	}
+
+	@Override
+    protected ASTExpression deepCloneWithIdentifierReplaced_inner(String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
+		return new ASTExpExists(
+		        exp.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+	}
+
+	@Override
+    public String getCanonicalForm() {
+		return exp.getCanonicalForm() + getNodeTypeSymbol();
+	}
+	
+	@Override
+    String getNodeTypeSymbol() {
+        return "??";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 1;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        return exp;
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.LEFT_HAND_OPERAND;
+    }
+	
+}