Merge branch 'master' into unboundTemplate

Conflicts:
	src/main/java/freemarker/core/Macro.java
	src/main/java/freemarker/core/_CoreAPI.java
diff --git a/src/main/java/freemarker/core/AddConcatExpression.java b/src/main/java/freemarker/core/AddConcatExpression.java
index 2c73509..164f097 100644
--- a/src/main/java/freemarker/core/AddConcatExpression.java
+++ b/src/main/java/freemarker/core/AddConcatExpression.java
@@ -60,7 +60,7 @@
             ArithmeticEngine ae =
                 env != null
                     ? env.getArithmeticEngine()
-                    : getTemplate().getArithmeticEngine();
+                    : getUnboundTemplate().getConfiguration().getArithmeticEngine();
             return new SimpleNumber(ae.add(first, second));
         }
         else if(leftModel instanceof TemplateSequenceModel && rightModel instanceof TemplateSequenceModel)
diff --git a/src/main/java/freemarker/core/ArithmeticExpression.java b/src/main/java/freemarker/core/ArithmeticExpression.java
index efd332b..90320c8 100644
--- a/src/main/java/freemarker/core/ArithmeticExpression.java
+++ b/src/main/java/freemarker/core/ArithmeticExpression.java
@@ -51,7 +51,7 @@
         ArithmeticEngine ae = 
             env != null 
                 ? env.getArithmeticEngine()
-                : getTemplate().getArithmeticEngine();
+                : getUnboundTemplate().getConfiguration().getArithmeticEngine();
         switch (operator) {
             case TYPE_SUBSTRACTION : 
                 return new SimpleNumber(ae.subtract(lhoNumber, rhoNumber));
diff --git a/src/main/java/freemarker/core/AssignmentInstruction.java b/src/main/java/freemarker/core/AssignmentInstruction.java
index de0c7b9..d428f86 100644
--- a/src/main/java/freemarker/core/AssignmentInstruction.java
+++ b/src/main/java/freemarker/core/AssignmentInstruction.java
@@ -106,7 +106,7 @@
         super.postParseCleanup(stripWhitespace);
         if (nestedElements.size() == 1) {
             Assignment ass = (Assignment) nestedElements.get(0);
-            ass.setLocation(getTemplate(), this, this);
+            ass.setLocation(getUnboundTemplate(), this, this);
             return ass;
         } 
         return this;
diff --git a/src/main/java/freemarker/core/BodyInstruction.java b/src/main/java/freemarker/core/BodyInstruction.java
index 0fd2d5a..4612603 100644
--- a/src/main/java/freemarker/core/BodyInstruction.java
+++ b/src/main/java/freemarker/core/BodyInstruction.java
@@ -105,7 +105,7 @@
     */
     
     class Context implements LocalContext {
-        Macro.Context invokingMacroContext;
+        CallableInvocationContext invokingMacroContext;
         Environment.Namespace bodyVars;
         
         Context(Environment env) throws TemplateException {
diff --git a/src/main/java/freemarker/core/BoundCallable.java b/src/main/java/freemarker/core/BoundCallable.java
new file mode 100644
index 0000000..efda4ef
--- /dev/null
+++ b/src/main/java/freemarker/core/BoundCallable.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package freemarker.core;
+
+import java.io.IOException;
+
+import freemarker.core.Environment.Namespace;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+
+/**
+ * A macro or function (or other future callable entity) associated to a namespace and a template.
+ * 
+ * <p>
+ * With an analogy, a {@link UnboundCallable} is like a non-static {@link java.lang.reflect.Method} in Java; it
+ * describes everything about the method, but it isn't bound to any object on which the method could be called.
+ * Continuing this analogy, a {@link BoundCallable} is like a {@link java.lang.reflect.Method} paired with the object
+ * whose method it is (the {@code this} object), and is thus callable in itself. In the case of FTL macros and FTL
+ * functions, instead of a single {@code this} object, we have two such objects: a namespace and a template. (One may
+ * wonder why the namespace is not enough, given that a namespace already specifies a template (
+ * {@link Namespace#getTemplate()} ). It's because a namespace can contain macros from included templates, and so the
+ * template that the callable belongs to isn't always the same as {@link Namespace#getTemplate()}, which just gives the
+ * "root" template of the namespace. Furthermore, several namespaces my include exactly the same template, so we can't
+ * get away with a template instead of a namespace either. Also note that knowing which template we are in is needed for
+ * example to resolve relative references to other templates.)
+ * 
+ * <p>
+ * Historical note: Prior to 2.4, the two concepts ({@link UnboundCallable} and {@link BoundCallable}) were these same,
+ * represented by {@link Macro}, which still exists due to backward compatibility constraints. This class extends
+ * {@link Macro} only for the sake of legacy applications which expect macro and function FTL variables to be
+ * {@link Macro}-s. Especially this class should not extend {@link TemplateElement} (which it does, because
+ * {@link Macro} is a subclass of that), but it had to, for backward compatibility. It just delegates {@link Macro}
+ * methods to the embedded {@link UnboundCallable}.
+ * 
+ * @see UnboundCallable
+ * 
+ * @since 2.4.0
+ */
+final class BoundCallable extends Macro {
+    
+    private final UnboundCallable unboundCallable;
+    private final Template template;
+    private final Namespace namespace;
+    
+    BoundCallable(UnboundCallable callableDefinition, Template template, Namespace namespace) {
+        this.unboundCallable = callableDefinition;
+        this.template = template;
+        this.namespace = namespace;
+    }
+
+    UnboundCallable getUnboundCallable() {
+        return unboundCallable;
+    }
+    
+    Template getTemplate() {
+        return template;
+    }
+    
+    Namespace getNamespace() {
+        return namespace;
+    }
+
+    @Override
+    public String toString() {
+        return "BoundCallable("
+                + "name=" + getName()
+                + ", isFunction=" + isFunction()
+                + ", template" + (template != null ? ".name=" + template.getName() : "=null")
+                + ", namespace=" + (namespace != null ? namespace.getTemplate().getName() : "null")
+                + ")";
+    }
+
+    /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+    @Override
+    public String getCatchAll() {
+        return unboundCallable.getCatchAll();
+    }
+
+    /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+    @Override
+    public String[] getArgumentNames() {
+        return unboundCallable.getArgumentNames();
+    }
+
+    /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+    @Override
+    public String getName() {
+        return unboundCallable.getName();
+    }
+
+    /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+    @Override
+    public boolean isFunction() {
+        return unboundCallable.isFunction();
+    }
+
+    /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+    @Override
+    void accept(Environment env) throws TemplateException, IOException {
+        unboundCallable.accept(env);
+    }
+
+    /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+    @Override
+    protected String dump(boolean canonical) {
+        return unboundCallable.dump(canonical);
+    }
+
+    /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+    @Override
+    String getNodeTypeSymbol() {
+        return unboundCallable.getNodeTypeSymbol();
+    }
+
+    /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+    @Override
+    int getParameterCount() {
+        return unboundCallable.getParameterCount();
+    }
+
+    /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+    @Override
+    Object getParameterValue(int idx) {
+        return unboundCallable.getParameterValue(idx);
+    }
+
+    /** For backward compatibility only; delegates to the {@link UnboundCallable}'s identical method. */
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return unboundCallable.getParameterRole(idx);
+    }
+    
+}
diff --git a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
index 99fc0b2..2f95122 100644
--- a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
+++ b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
@@ -308,8 +308,9 @@
         TemplateModel _eval(Environment env) throws TemplateException {
             TemplateModel tm = target.eval(env);
             target.assertNonNull(tm, env);
-            // WRONG: it also had to check Macro.isFunction()
-            return (tm instanceof TemplateTransformModel || tm instanceof Macro || tm instanceof TemplateDirectiveModel) ?
+            // [2.4] WRONG: it also had to check Macro.isFunction()
+            return (tm instanceof TemplateTransformModel || tm instanceof BoundCallable
+                    || tm instanceof TemplateDirectiveModel) ?
                 TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
         }
     }
@@ -355,7 +356,7 @@
             TemplateModel tm = target.eval(env);
             target.assertNonNull(tm, env);
             // WRONG: it also had to check Macro.isFunction()
-            return (tm instanceof Macro)  ?
+            return (tm instanceof BoundCallable)  ?
                 TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
         }
     }
@@ -416,13 +417,13 @@
     static class namespaceBI extends BuiltIn {
         TemplateModel _eval(Environment env) throws TemplateException {
             TemplateModel tm = target.eval(env);
-            if (!(tm instanceof Macro)) {
+            if (!(tm instanceof BoundCallable)) {
                 throw new UnexpectedTypeException(
                         target, tm,
-                        "macro or function", new Class[] { Macro.class },
+                        "macro or function", new Class[] { BoundCallable.class },
                         env);
             } else {
-                return env.getMacroNamespace((Macro) tm);
+                return ((BoundCallable) tm).getNamespace();
             }
         }
     }
diff --git a/src/main/java/freemarker/core/BuiltInsForStringsMisc.java b/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
index bf767d0..90efbee 100644
--- a/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
+++ b/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
@@ -54,13 +54,13 @@
             token_source.incompatibleImprovements = _TemplateAPI.getTemplateLanguageVersionAsInt(this);
             token_source.SwitchTo(FMParserConstants.FM_EXPRESSION);
             FMParser parser = new FMParser(token_source);
-            parser.setTemplate(getTemplate());
+            parser.setTemplate(getUnboundTemplate());
             Expression exp = null;
             try {
                 try {
                     exp = parser.Expression();
                 } catch (TokenMgrError e) {
-                    throw e.toParseException(getTemplate());
+                    throw e.toParseException(getUnboundTemplate());
                 }
             } catch (ParseException e) {
                 throw new _MiscTemplateException(this, env, new Object[] {
diff --git a/src/main/java/freemarker/core/BuiltinVariable.java b/src/main/java/freemarker/core/BuiltinVariable.java
index c1de72e..af89a89 100644
--- a/src/main/java/freemarker/core/BuiltinVariable.java
+++ b/src/main/java/freemarker/core/BuiltinVariable.java
@@ -53,6 +53,8 @@
     static final String URL_ESCAPING_CHARSET = "url_escaping_charset";
     static final String NOW = "now";
     
+    private static final BoundCallable PASS_VALUE = new BoundCallable(UnboundCallable.NO_OP_MACRO, null, null);
+    
     static final String[] SPEC_VAR_NAMES = new String[] {
         CURRENT_NODE,
         DATA_MODEL,
@@ -112,7 +114,7 @@
             return env.getGlobalVariables();
         }
         if (name == LOCALS) {
-            Macro.Context ctx = env.getCurrentMacroContext();
+            CallableInvocationContext ctx = env.getCurrentMacroContext();
             return ctx == null ? null : ctx.getLocals();
         }
         if (name == DATA_MODEL) {
@@ -137,7 +139,7 @@
             return new SimpleScalar(env.getTemplate().getName());
         }
         if (name == PASS) {
-            return Macro.DO_NOTHING_MACRO;
+            return PASS_VALUE;
         }
         if (name == VERSION) {
             return new SimpleScalar(Configuration.getVersionNumber());
diff --git a/src/main/java/freemarker/core/CallableInvocationContext.java b/src/main/java/freemarker/core/CallableInvocationContext.java
new file mode 100644
index 0000000..bc83f36
--- /dev/null
+++ b/src/main/java/freemarker/core/CallableInvocationContext.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package freemarker.core;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateModelIterator;
+
+/**
+ * The local variables and such of an FTL macro or FTL function (or other future FTL callable) call.
+ */
+class CallableInvocationContext implements LocalContext {
+    final UnboundCallable callableDefinition;
+    final Environment.Namespace localVars; 
+    final TemplateElement nestedContent;
+    final Environment.Namespace nestedContentNamespace;
+    final Template nestedContentTemplate;
+    final List nestedContentParameterNames;
+    final ArrayList prevLocalContextStack;
+    final CallableInvocationContext prevMacroContext;
+    
+    CallableInvocationContext(UnboundCallable callableDefinition,
+            Environment env, 
+            TemplateElement nestedContent,
+            List nestedContentParameterNames) 
+    {
+        this.callableDefinition = callableDefinition;
+        this.localVars = env.new Namespace();
+        this.nestedContent = nestedContent;
+        this.nestedContentNamespace = env.getCurrentNamespace();
+        this.nestedContentTemplate = env.getCurrentTemplate();
+        this.nestedContentParameterNames = nestedContentParameterNames;
+        this.prevLocalContextStack = env.getLocalContextStack();
+        this.prevMacroContext = env.getCurrentMacroContext();
+    }
+    
+    Macro getCallableDefinition() {
+        return callableDefinition;
+    }
+
+    void invoce(Environment env) throws TemplateException, IOException {
+        sanityCheck(env);
+        // Set default values for unspecified parameters
+        if (callableDefinition.nestedBlock != null) {
+            env.visit(callableDefinition.nestedBlock);
+        }
+    }
+
+    // Set default parameters, check if all the required parameters are defined.
+    void sanityCheck(Environment env) throws TemplateException {
+        boolean resolvedAnArg, hasUnresolvedArg;
+        Expression firstUnresolvedExpression;
+        InvalidReferenceException firstReferenceException;
+        do {
+            firstUnresolvedExpression = null;
+            firstReferenceException = null;
+            resolvedAnArg = hasUnresolvedArg = false;
+            for(int i = 0; i < callableDefinition.getParamNames().length; ++i) {
+                String argName = callableDefinition.getParamNames()[i];
+                if(localVars.get(argName) == null) {
+                    Expression valueExp = (Expression) callableDefinition.getParamDefaults().get(argName);
+                    if (valueExp != null) {
+                        try {
+                            TemplateModel tm = valueExp.eval(env);
+                            if(tm == null) {
+                                if(!hasUnresolvedArg) {
+                                    firstUnresolvedExpression = valueExp;
+                                    hasUnresolvedArg = true;
+                                }
+                            }
+                            else {
+                                localVars.put(argName, tm);
+                                resolvedAnArg = true;
+                            }
+                        }
+                        catch(InvalidReferenceException e) {
+                            if(!hasUnresolvedArg) {
+                                hasUnresolvedArg = true;
+                                firstReferenceException = e;
+                            }
+                        }
+                    }
+                    else if (!env.isClassicCompatible()) {
+                        boolean argWasSpecified = localVars.containsKey(argName);
+                        throw new _MiscTemplateException(env,
+                                new _ErrorDescriptionBuilder(new Object[] {
+                                        "When calling macro ", new _DelayedJQuote(callableDefinition.getName()), 
+                                        ", required parameter ", new _DelayedJQuote(argName),
+                                        " (parameter #", new Integer(i + 1), ") was ", 
+                                        (argWasSpecified
+                                                ? "specified, but had null/missing value."
+                                                : "not specified.") 
+                                }).tip(argWasSpecified
+                                        ? new Object[] {
+                                                "If the parameter value expression on the caller side is known to "
+                                                + "be legally null/missing, you may want to specify a default "
+                                                + "value for it with the \"!\" operator, like "
+                                                + "paramValue!defaultValue." }
+                                        : new Object[] { 
+                                                "If the omission was deliberate, you may consider making the "
+                                                + "parameter optional in the macro by specifying a default value "
+                                                + "for it, like ", "<#macro macroName paramName=defaultExpr>", ")" }
+                                        ));
+                    }
+                }
+            }
+        }
+        while(resolvedAnArg && hasUnresolvedArg);
+        if(hasUnresolvedArg) {
+            if(firstReferenceException != null) {
+                throw firstReferenceException;
+            } else if (!env.isClassicCompatible()) {
+                throw InvalidReferenceException.getInstance(firstUnresolvedExpression, env);
+            }
+        }
+    }
+
+    /**
+     * @return the local variable of the given name
+     * or null if it doesn't exist.
+     */ 
+    public TemplateModel getLocalVariable(String name) throws TemplateModelException {
+         return localVars.get(name);
+    }
+
+    Environment.Namespace getLocals() {
+        return localVars;
+    }
+    
+    /**
+     * Set a local variable in this macro 
+     */
+    void setLocalVar(String name, TemplateModel var) {
+        localVars.put(name, var);
+    }
+
+    public Collection getLocalVariableNames() throws TemplateModelException {
+        HashSet result = new HashSet();
+        for (TemplateModelIterator it = localVars.keys().iterator(); it.hasNext();) {
+            result.add(it.next().toString());
+        }
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java
index 8538c95..0ae4dd5 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -27,7 +27,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
-import java.util.LinkedList;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -48,6 +48,7 @@
 import freemarker.template.TemplateModel;
 import freemarker.template.Version;
 import freemarker.template._TemplateAPI;
+import freemarker.template.utility.CollectionUtils;
 import freemarker.template.utility.NullArgumentException;
 import freemarker.template.utility.StringUtil;
 
@@ -126,8 +127,12 @@
     };
 
     private Configurable parent;
+    
     private Properties properties;
-    private HashMap customAttributes;
+    
+    private LinkedHashMap<Object, Object> customAttributes;
+    // Can't be final because we are cloneable:
+    private Object customAttributesLock = new Object();
     
     private Locale locale;
     private String numberFormat;
@@ -228,8 +233,6 @@
         // which means "not specified"
 
         setBooleanFormat(C_TRUE_FALSE);
-        
-        customAttributes = new HashMap();
     }
 
     /**
@@ -243,13 +246,13 @@
         classicCompatible = null;
         templateExceptionHandler = null;
         properties = new Properties(parent.properties);
-        customAttributes = new HashMap();
     }
     
     protected Object clone() throws CloneNotSupportedException {
         Configurable copy = (Configurable)super.clone();
         copy.properties = new Properties(properties);
-        copy.customAttributes = (HashMap)customAttributes.clone();
+        copy.customAttributesLock = new Object();
+        copy.customAttributes = customAttributes == null ? null : (LinkedHashMap) customAttributes.clone();
         return copy;
     }
     
@@ -1479,6 +1482,10 @@
     /** Returns the possible setting names. */
     // [Java 5] Add type param. [FM 2.4] It must return the camelCase names, then make it public.
     Set/*<String>*/ getSettingNames() {
+        return getConfigurableSettingNames(); 
+    }
+    
+    static Set<String> getConfigurableSettingNames() {
         return new _SortedArraySet(SETTING_NAMES); 
     }
 
@@ -1643,29 +1650,49 @@
     }
 
     /**
-     * Internal entry point for setting unnamed custom attributes
+     * Used internally for setting custom attributes, both named and unnamed ones.
      */
     void setCustomAttribute(Object key, Object value) {
-        synchronized(customAttributes) {
+        synchronized (customAttributesLock) {
+            LinkedHashMap<Object, Object> customAttributes = this.customAttributes;
+            if (customAttributes == null) {
+                customAttributes = createInitialCustomAttributes();
+                this.customAttributes = customAttributes;
+            }
             customAttributes.put(key, value);
         }
     }
 
     /**
-     * Internal entry point for getting unnamed custom attributes
+     * User internally for getting unnamed custom attributes.
      */
     Object getCustomAttribute(Object key, CustomAttribute attr) {
-        synchronized(customAttributes) {
-            Object o = customAttributes.get(key);
-            if(o == null && !customAttributes.containsKey(key)) {
-                o = attr.create();
-                customAttributes.put(key, o);
+        synchronized (customAttributesLock) {
+            LinkedHashMap<Object, Object> customAttributes = this.customAttributes;
+            Object value = customAttributes != null ? customAttributes.get(key) : null;
+            if(value == null && (customAttributes == null || !customAttributes.containsKey(key))) {
+                value = attr.create();
+                if (customAttributes == null) {
+                    customAttributes = createInitialCustomAttributes();
+                    this.customAttributes = customAttributes;
+                }
+                customAttributes.put(key, value);
             }
-            return o;
+            return value;
         }
     }
     
     /**
+     * Returns the non-{@code null} writable initial custom attribute map.
+     */
+    private LinkedHashMap<Object, Object> createInitialCustomAttributes() {
+        final Map<String, ?> initialCustomAttributes = getInitialCustomAttributes();
+        return initialCustomAttributes == null
+                ? new LinkedHashMap<Object, Object>()
+                : new LinkedHashMap<Object, Object>(initialCustomAttributes);
+    }
+    
+    /**
      * Sets a named custom attribute for this configurable.
      *
      * @param name the name of the custom attribute
@@ -1675,28 +1702,44 @@
      * {@link #removeCustomAttribute(String)}.
      */
     public void setCustomAttribute(String name, Object value) {
-        synchronized(customAttributes) {
-            customAttributes.put(name, value);
-        }
+        setCustomAttribute((Object) name, value);
     }
     
     /**
-     * Returns an array with names of all custom attributes defined directly 
-     * on this configurable. (That is, it doesn't contain the names of custom attributes
-     * defined indirectly on its parent configurables.) The returned array is never null,
+     * Returns an array that contains the snapshot of the names of all custom attributes defined directly 
+     * in this configurable. (That is, it doesn't contain the names of custom attributes
+     * defined indirectly on its parent configurables.) The returned array is never {@code null},
      * but can be zero-length.
-     * The order of elements in the returned array is not defined and can change
-     * between invocations.  
+     * Since 2.4.0, the order of the names is the same as the attributes were added. Before 2.4.0, the order was
+     * undefined.  
      */
     public String[] getCustomAttributeNames() {
-        synchronized(customAttributes) {
-            Collection names = new LinkedList(customAttributes.keySet());
-            for (Iterator iter = names.iterator(); iter.hasNext();) {
-                if(!(iter.next() instanceof String)) {
-                    iter.remove();
+        synchronized(customAttributesLock) {
+            final LinkedHashMap<Object, Object> customAttributes = this.customAttributes;
+            if (customAttributes == null) {
+                return getInitialCustomAttributeNames();
+            }
+            
+            Set<Object> keys = customAttributes.keySet();
+            int stringKeyCnt = 0;
+            for (Object key : keys) {
+                if (key instanceof String) {
+                    stringKeyCnt++;
                 }
             }
-            return (String[])names.toArray(new String[names.size()]);
+            
+            if (stringKeyCnt == 0) {
+                return CollectionUtils.EMPTY_STRING_ARRAY;
+            }
+            
+            String[] result = new String[stringKeyCnt];
+            int i = 0;
+            for (Object key : keys) {
+                if (key instanceof String) {
+                    result[i++] = (String) key;
+                }
+            }
+            return result;
         }
     }
     
@@ -1711,8 +1754,19 @@
      * @param name the name of the custom attribute
      */
     public void removeCustomAttribute(String name) {
-        synchronized(customAttributes) {
-            customAttributes.remove(name);
+        synchronized(customAttributesLock) {
+            LinkedHashMap<Object, Object> customAttributes = this.customAttributes;
+            if (customAttributes != null) {
+                customAttributes.remove(name);
+            } else {
+                Map<String, ?> initialCustomAttributes = getInitialCustomAttributes();
+                if (initialCustomAttributes == null || !initialCustomAttributes.containsKey(name)) {
+                    return;
+                }
+                customAttributes = createInitialCustomAttributes();
+                this.customAttributes = customAttributes;
+                customAttributes.remove(name);
+            }
         }
     }
 
@@ -1730,18 +1784,65 @@
      */
     public Object getCustomAttribute(String name) {
         Object retval;
-        synchronized(customAttributes) {
-            retval = customAttributes.get(name);
-            if(retval == null && customAttributes.containsKey(name)) {
-                return null;
+        synchronized (customAttributesLock) {
+            final LinkedHashMap<Object, Object> customAttributes = this.customAttributes;
+            if (customAttributes == null) {
+                Map<String, ?> initialCustomAttributes = getInitialCustomAttributes();
+                if (initialCustomAttributes == null) {
+                    retval = null;
+                } else {
+                    retval = initialCustomAttributes.get(name);
+                    if (retval == null && initialCustomAttributes.containsKey(name)) {
+                        return null;
+                    }
+                }
+            } else {
+                retval = customAttributes.get(name);
+                if (retval == null && customAttributes.containsKey(name)) {
+                    return null;
+                }
             }
         }
-        if(retval == null && parent != null) {
+        if (retval == null && parent != null) {
             return parent.getCustomAttribute(name);
         }
         return retval;
     }
     
+    /**
+     * Returns the initial (default) set of custom attributes, or {@code null}. The returned {@link Map} must not be
+     * modified. It shouldn't be accessed during template parsing, as during that the content is possibly changing.
+     * 
+     * <p>
+     * This was added so that it can be overidden in {@link Template}, where it gives the custom attributes defined in
+     * the {@code #ftl} header. The returned {@link Map} must not be modified! When the custom attributes need to be
+     * written, a copy of this {@link Map} will be made, modified, and after that this {@link Map} isn't used anymore
+     * from this {@link Configurable} instance.
+     * 
+     * @since 2.4.0
+     */
+    protected Map<String, ?> getInitialCustomAttributes() {
+        return null;
+    }
+
+    /**
+     * Returns the initial (default) set of custom attribute names (maybe an empty array, but not {@code null}).
+     */
+    private String[] getInitialCustomAttributeNames() {
+        Map<String, ?> initalCustomAttributes = getInitialCustomAttributes();
+        if (initalCustomAttributes == null) {
+            return CollectionUtils.EMPTY_STRING_ARRAY;
+        }
+        
+        final Set<String> keys = initalCustomAttributes.keySet();
+        final String[] result = new String[keys.size()];
+        int i = 0;
+        for (String key : keys) {
+            result[i++] = key;
+        }
+        return result;
+    }
+
     protected void doAutoImportsAndIncludes(Environment env)
     throws TemplateException, IOException
     {
diff --git a/src/main/java/freemarker/core/DebugBreak.java b/src/main/java/freemarker/core/DebugBreak.java
index 42cf053..d3a82e6 100644
--- a/src/main/java/freemarker/core/DebugBreak.java
+++ b/src/main/java/freemarker/core/DebugBreak.java
@@ -38,7 +38,7 @@
     
     protected void accept(Environment env) throws TemplateException, IOException
     {
-        if(!DebuggerService.suspendEnvironment(env, this.getTemplate().getSourceName(), nestedBlock.getBeginLine()))
+        if(!DebuggerService.suspendEnvironment(env, this.getUnboundTemplate().getSourceName(), nestedBlock.getBeginLine()))
         {
             nestedBlock.accept(env);
         }
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index 59568e9..9fb2d43 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -67,6 +67,7 @@
 import freemarker.template.utility.DateUtil;
 import freemarker.template.utility.DateUtil.DateToISO8601CalendarFactory;
 import freemarker.template.utility.NullWriter;
+import freemarker.template.utility.StringUtil;
 import freemarker.template.utility.UndeclaredThrowableException;
 
 /**
@@ -153,18 +154,20 @@
     
     private Collator cachedCollator;
 
+    private Template currentTemplate;
+    private Namespace currentNamespace;
+    private CallableInvocationContext currentMacroContext;
+    
     private Writer out;
-    private Macro.Context currentMacroContext;
     private ArrayList localContextStack; 
     private final Namespace mainNamespace;
-    private Namespace currentNamespace, globalNamespace;
+    private Namespace globalNamespace;
     private HashMap loadedLibs;
 
     private boolean inAttemptBlock;
     private Throwable lastThrowable;
     
     private TemplateModel lastReturnValue;
-    private HashMap macroToNamespaceLookup = new HashMap();
 
     private TemplateNodeModel currentVisitorNode;    
     private TemplateSequenceModel nodeNamespaces;
@@ -194,9 +197,10 @@
         super(template);
         this.globalNamespace = new Namespace(null);
         this.currentNamespace = mainNamespace = new Namespace(template);
+        this.currentTemplate = getMainTemplate();
         this.out = out;
         this.rootDataModel = rootDataModel;
-        importMacros(template);
+        predefineMacros(template);
     }
 
     /**
@@ -205,9 +209,9 @@
      * the {@link Environment} parent switchings that occur at {@code #include}/{@code #import} and {@code #nested}
      * directive calls, that is, it's not very meaningful outside FreeMarker internals.
      * 
-     * @deprecated Use {@link #getMainTemplate()} instead (or {@link #getCurrentNamespace()} and then
-     *             {@link Namespace#getTemplate()}); the value returned by this method is often not what you expect when
-     *             it comes to macro/function invocations.
+     * @deprecated Use {@link #getMainTemplate()} or {@link #getCurrentTemplate()} (also relevant,
+     *             {@link #getCurrentNamespace()} and then {@link Namespace#getTemplate()}); the value returned by this
+     *             method is often not what you expect when it comes to macro/function invocations.
      */
     public Template getTemplate() {
         return (Template)getParent();
@@ -226,17 +230,18 @@
     }
     
     /**
-     * Used only internally as of yet, no backward compatibility - Returns the {@link Template} that we are "lexically"
-     * inside at moment. This template will change when entering an {@code #include} or calling a macro or function in
-     * another template, or returning to yet another template with {@code #nested}. As such, it's useful in
-     * {@link TemplateDirectiveModel} to find out if from where the directive was called.
+     * Returns the {@link Template} that we are "lexically" inside at the moment. This template will change when
+     * entering an {@code #include} or calling a macro or function in another template, or returning into another
+     * template with {@code #nested}. As such, it's useful in {@link TemplateDirectiveModel} to find out if from where
+     * the directive was called from.
      * 
      * @see #getMainTemplate()
      * @see #getCurrentNamespace()
+     * 
+     * @since 2.4.0
      */
-    Template getCurrentTemplate() {
-        int ln = instructionStack.size();
-        return ln == 0 ? getMainTemplate() : ((TemplateObject) instructionStack.get(ln - 1)).getTemplate();
+    public Template getCurrentTemplate() {
+        return currentTemplate;
     }
 
     /**
@@ -510,16 +515,20 @@
      * Used for {@code #nested}.
      */
     void invokeNestedContent(BodyInstruction.Context bodyCtx) throws TemplateException, IOException {
-        Macro.Context invokingMacroContext = getCurrentMacroContext();
+        CallableInvocationContext invokingMacroContext = getCurrentMacroContext();
         ArrayList prevLocalContextStack = localContextStack;
         TemplateElement nestedContent = invokingMacroContext.nestedContent;
         if (nestedContent != null) {
             this.currentMacroContext = invokingMacroContext.prevMacroContext;
+            
+            final Namespace prevCurrentNamespace = currentNamespace;  
             currentNamespace = invokingMacroContext.nestedContentNamespace;
             
+            final Template prevCurrentTemplate = currentTemplate;
+            currentTemplate = invokingMacroContext.nestedContentTemplate;
+            
             final Configurable prevParent;
-            final boolean parentReplacementOn
-                    = isIcI2322OrLater();
+            final boolean parentReplacementOn = isIcI2322OrLater();
             if (parentReplacementOn) {
                 prevParent = getParent();
                 setParent(currentNamespace.getTemplate());
@@ -539,7 +548,8 @@
                     popLocalContext();
                 }
                 this.currentMacroContext = invokingMacroContext;
-                currentNamespace = getMacroNamespace(invokingMacroContext.getMacro());
+                currentNamespace = prevCurrentNamespace;
+                currentTemplate = prevCurrentTemplate;
                 if (parentReplacementOn) {
                     setParent(prevParent);
                 }
@@ -590,8 +600,8 @@
         }
         try {
             TemplateModel macroOrTransform = getNodeProcessor(node);
-            if (macroOrTransform instanceof Macro) {
-                invoke((Macro) macroOrTransform, null, null, null, null);
+            if (macroOrTransform instanceof BoundCallable) {
+                invoke((BoundCallable) macroOrTransform, null, null, null, null);
             }
             else if (macroOrTransform instanceof TemplateTransformModel) {
                 visitAndTransform(null, (TemplateTransformModel) macroOrTransform, null); 
@@ -653,8 +663,8 @@
     
     void fallback() throws TemplateException, IOException {
         TemplateModel macroOrTransform = getNodeProcessor(currentNodeName, currentNodeNS, nodeNamespaceIndex);
-        if (macroOrTransform instanceof Macro) {
-            invoke((Macro) macroOrTransform, null, null, null, null);
+        if (macroOrTransform instanceof BoundCallable) {
+            invoke((BoundCallable) macroOrTransform, null, null, null, null);
         }
         else if (macroOrTransform instanceof TemplateTransformModel) {
             visitAndTransform(null, (TemplateTransformModel) macroOrTransform, null); 
@@ -664,26 +674,30 @@
     /**
      * Calls the macro or function with the given arguments and nested block.
      */
-    void invoke(Macro macro, 
+    void invoke(BoundCallable boundCallable, 
                Map namedArgs, List positionalArgs, 
                List bodyParameterNames, TemplateElement nestedBlock) throws TemplateException, IOException {
-        if (macro == Macro.DO_NOTHING_MACRO) {
+        UnboundCallable unboundCallable = boundCallable.getUnboundCallable();
+        if (unboundCallable == UnboundCallable.NO_OP_MACRO) {
             return;
         }
         
-        pushElement(macro);
+        pushElement(unboundCallable);
         try {
-            final Macro.Context macroCtx = macro.new Context(this, nestedBlock, bodyParameterNames);
-            setMacroContextLocalsFromArguments(macroCtx, macro, namedArgs, positionalArgs);
+            final CallableInvocationContext macroCtx = new CallableInvocationContext(unboundCallable, this, nestedBlock, bodyParameterNames);
+            setMacroContextLocalsFromArguments(macroCtx, unboundCallable, namedArgs, positionalArgs);
             
-            final Macro.Context prevMacroCtx = currentMacroContext;
+            final CallableInvocationContext prevMacroCtx = currentMacroContext;
             currentMacroContext = macroCtx;
             
             final ArrayList prevLocalContextStack = localContextStack;
             localContextStack = null;
             
-            final Namespace prevNamespace = currentNamespace;
-            currentNamespace = (Namespace) macroToNamespaceLookup.get(macro);
+            final Namespace prevCurrentNamespace = currentNamespace;
+            currentNamespace = boundCallable.getNamespace();
+            
+            final Template prevCurrentTemplate = currentTemplate;
+            currentTemplate = boundCallable.getTemplate();
             
             final Configurable prevParent;
             final boolean parentReplacementOn
@@ -697,7 +711,7 @@
             }
             
             try {
-                macroCtx.runMacro(this);
+                macroCtx.invoce(this);
             } catch (ReturnInstruction.Return re) {
                 // Not an error, just a <#return>
             } catch (TemplateException te) {
@@ -705,7 +719,8 @@
             } finally {
                 currentMacroContext = prevMacroCtx;
                 localContextStack = prevLocalContextStack;
-                currentNamespace = prevNamespace;
+                currentNamespace = prevCurrentNamespace;
+                currentTemplate = prevCurrentTemplate;
                 if (parentReplacementOn) {
                     setParent(prevParent);
                 }
@@ -719,10 +734,10 @@
      * Sets the local variables corresponding to the macro call arguments in the macro context.
      */
     private void setMacroContextLocalsFromArguments(
-            final Macro.Context macroCtx,
-            final Macro macro,
+            final CallableInvocationContext macroCtx,
+            final UnboundCallable unboundCallable,
             final Map namedArgs, final List positionalArgs) throws TemplateException, _MiscTemplateException {
-        String catchAllParamName = macro.getCatchAll();
+        String catchAllParamName = unboundCallable.getCatchAll();
         if (namedArgs != null) {
             final SimpleHash catchAllParamValue;
             if (catchAllParamName != null) {
@@ -735,7 +750,7 @@
             for (Iterator it = namedArgs.entrySet().iterator(); it.hasNext();) {
                 final Map.Entry argNameAndValExp = (Map.Entry) it.next();
                 final String argName = (String) argNameAndValExp.getKey();
-                final boolean isArgNameDeclared = macro.hasArgNamed(argName);
+                final boolean isArgNameDeclared = unboundCallable.hasArgNamed(argName);
                 if (isArgNameDeclared || catchAllParamName != null) {
                     Expression argValueExp = (Expression) argNameAndValExp.getValue();
                     TemplateModel argValue = argValueExp.eval(this);
@@ -746,7 +761,7 @@
                     }
                 } else {
                     throw new _MiscTemplateException(this, new Object[] {
-                            (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()),
+                            (unboundCallable.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(unboundCallable.getName()),
                             " has no parameter with name ", new _DelayedJQuote(argName), "." });
                 }
             }
@@ -759,11 +774,11 @@
                 catchAllParamValue = null;
             }
             
-            String[] argNames = macro.getArgumentNamesInternal();
+            String[] argNames = unboundCallable.getArgumentNamesInternal();
             final int argsCnt = positionalArgs.size();
             if (argNames.length < argsCnt && catchAllParamName == null) {
                 throw new _MiscTemplateException(this, new Object[] { 
-                        (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()),
+                        (unboundCallable.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(unboundCallable.getName()),
                         " only accepts ", new _DelayedToString(argNames.length), " parameters, but got ",
                         new _DelayedToString(argsCnt), "."});
             }
@@ -787,13 +802,10 @@
     /**
      * Defines the given macro in the current namespace (doesn't call it).
      */
-    void visitMacroDef(Macro macro) {
-        macroToNamespaceLookup.put(macro, currentNamespace);
-        currentNamespace.put(macro.getName(), macro);
-    }
-    
-    Namespace getMacroNamespace(Macro macro) {
-        return (Namespace) macroToNamespaceLookup.get(macro);
+    void visitCallableDefinition(UnboundCallable unboundCallable) {
+        currentNamespace.put(
+                unboundCallable.getName(),
+                new BoundCallable(unboundCallable, currentTemplate, currentNamespace));
     }
     
     void recurse(TemplateNodeModel node, TemplateSequenceModel namespaces)
@@ -816,7 +828,7 @@
         }
     }
 
-    Macro.Context getCurrentMacroContext() {
+    CallableInvocationContext getCurrentMacroContext() {
         return currentMacroContext;
     }
     
@@ -1707,7 +1719,7 @@
                     enclosingMacro, stackEl.beginLine, stackEl.beginColumn));
         } else {
             sb.append(MessageUtil.formatLocationForEvaluationError(
-                    stackEl.getTemplate(), stackEl.beginLine, stackEl.beginColumn));
+                    stackEl.getUnboundTemplate(), stackEl.beginLine, stackEl.beginColumn));
         }
         sb.append("]");
     }
@@ -1929,7 +1941,7 @@
         TemplateModel result = null;
         if (nsURI == null) {
             result = ns.get(localName);
-            if (!(result instanceof Macro) && !(result instanceof TemplateTransformModel)) {
+            if (!(result instanceof BoundCallable) && !(result instanceof TemplateTransformModel)) {
                 result = null;
             }
         } else {
@@ -1942,25 +1954,25 @@
             }
             if (prefix.length() >0) {
                 result = ns.get(prefix + ":" + localName);
-                if (!(result instanceof Macro) && !(result instanceof TemplateTransformModel)) {
+                if (!(result instanceof BoundCallable) && !(result instanceof TemplateTransformModel)) {
                     result = null;
                 }
             } else {
                 if (nsURI.length() == 0) {
                     result = ns.get(Template.NO_NS_PREFIX + ":" + localName);
-                    if (!(result instanceof Macro) && !(result instanceof TemplateTransformModel)) {
+                    if (!(result instanceof BoundCallable) && !(result instanceof TemplateTransformModel)) {
                         result = null;
                     }
                 }
                 if (nsURI.equals(template.getDefaultNS())) {
                     result = ns.get(Template.DEFAULT_NAMESPACE_PREFIX + ":" + localName);
-                    if (!(result instanceof Macro) && !(result instanceof TemplateTransformModel)) {
+                    if (!(result instanceof BoundCallable) && !(result instanceof TemplateTransformModel)) {
                         result = null;
                     }
                 }
                 if (result == null) {
                     result = ns.get(localName);
-                    if (!(result instanceof Macro) && !(result instanceof TemplateTransformModel)) {
+                    if (!(result instanceof BoundCallable) && !(result instanceof TemplateTransformModel)) {
                         result = null;
                     }
                 }
@@ -2069,14 +2081,19 @@
             prevTemplate = null;
         }
         
-        importMacros(includedTemplate);
+        final Template prevCurrentTemplate = currentTemplate;
         try {
-            visit(includedTemplate.getRootTreeNode());
-        }
-        finally {
-            if (parentReplacementOn) {
-                setParent(prevTemplate);
+            currentTemplate = includedTemplate;
+            predefineMacros(includedTemplate);
+            try {
+                visit(includedTemplate.getRootTreeNode());
+            } finally {
+                if (parentReplacementOn) {
+                    setParent(prevTemplate);
+                }
             }
+        } finally {
+            currentTemplate = prevCurrentTemplate;
         }
     }
 
@@ -2201,9 +2218,9 @@
         }
     }
 
-    void importMacros(Template template) {
+    void predefineMacros(Template template) {
         for (Iterator it = template.getMacros().values().iterator(); it.hasNext();) {
-            visitMacroDef((Macro) it.next());
+            visitCallableDefinition((UnboundCallable) it.next());
         }
     }
 
@@ -2310,6 +2327,11 @@
         public Template getTemplate() {
             return template == null ? Environment.this.getTemplate() : template;
         }
+        
+        public String toString() {
+            return StringUtil.jQuote(template.getName()) + super.toString();
+        }
+        
     }
 
      private static final Writer EMPTY_BODY_WRITER = new Writer() {
diff --git a/src/main/java/freemarker/core/EvalUtil.java b/src/main/java/freemarker/core/EvalUtil.java
index 68c935a..e5bcb75 100644
--- a/src/main/java/freemarker/core/EvalUtil.java
+++ b/src/main/java/freemarker/core/EvalUtil.java
@@ -226,7 +226,7 @@
                     env != null
                         ? env.getArithmeticEngine()
                         : (leftExp != null
-                            ? leftExp.getTemplate().getArithmeticEngine()
+                            ? leftExp.getUnboundTemplate().getConfiguration().getArithmeticEngine()
                             : ArithmeticEngine.BIGDECIMAL_ENGINE);
             try {
                 cmpResult = ae.compareNumbers(leftNum, rightNum);
diff --git a/src/main/java/freemarker/core/Expression.java b/src/main/java/freemarker/core/Expression.java
index d49dabc..ba97419 100644
--- a/src/main/java/freemarker/core/Expression.java
+++ b/src/main/java/freemarker/core/Expression.java
@@ -18,7 +18,6 @@
 
 import freemarker.ext.beans.BeanModel;
 import freemarker.template.Configuration;
-import freemarker.template.Template;
 import freemarker.template.TemplateBooleanModel;
 import freemarker.template.TemplateCollectionModel;
 import freemarker.template.TemplateDateModel;
@@ -53,11 +52,11 @@
 
     // Hook in here to set the constant value if possible.
     
-    void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine)
+    void setLocation(UnboundTemplate unboundTemplate, int beginColumn, int beginLine, int endColumn, int endLine)
     throws
         ParseException
     {
-        super.setLocation(template, beginColumn, beginLine, endColumn, endLine);
+        super.setLocation(unboundTemplate, beginColumn, beginLine, endColumn, endLine);
         if (isLiteral()) {
             try {
                 constantValue = _eval(null);
diff --git a/src/main/java/freemarker/core/IfBlock.java b/src/main/java/freemarker/core/IfBlock.java
index eaac963..e076d73 100644
--- a/src/main/java/freemarker/core/IfBlock.java
+++ b/src/main/java/freemarker/core/IfBlock.java
@@ -58,7 +58,7 @@
         if (nestedElements.size() == 1) {
             ConditionalBlock cblock = (ConditionalBlock) nestedElements.get(0);
             cblock.isLonelyIf = true;
-            cblock.setLocation(getTemplate(), cblock, this);
+            cblock.setLocation(getUnboundTemplate(), cblock, this);
             return cblock.postParseCleanup(stripWhitespace);
         }
         else {
diff --git a/src/main/java/freemarker/core/Include.java b/src/main/java/freemarker/core/Include.java
index 9139e09..5f8703d 100644
--- a/src/main/java/freemarker/core/Include.java
+++ b/src/main/java/freemarker/core/Include.java
@@ -38,12 +38,12 @@
     private final Boolean ignoreMissing;
 
     /**
-     * @param template the template that this <tt>#include</tt> is a part of.
+     * @param unboundTemplate the template that this <tt>#include</tt> is a part of.
      * @param includedTemplatePathExp the path of the template to be included.
      * @param encodingExp the encoding to be used or null, if it's the default.
      * @param parseExp whether the template should be parsed (or is raw text)
      */
-    Include(Template template,
+    Include(UnboundTemplate unboundTemplate,
             Expression includedTemplatePathExp,
             Expression encodingExp, Expression parseExp, Expression ignoreMissingExp) throws ParseException {
         this.includedTemplateNameExp = includedTemplatePathExp;
@@ -80,7 +80,7 @@
                         parse = Boolean.valueOf(StringUtil.getYesNo(parseExp.evalAndCoerceToString(null)));
                     } else {
                         try {
-                            parse = Boolean.valueOf(parseExp.evalToBoolean(template.getConfiguration()));
+                            parse = Boolean.valueOf(parseExp.evalToBoolean(unboundTemplate.getConfiguration()));
                         } catch(NonBooleanException e) {
                             throw new ParseException("Expected a boolean or string as the value of the parse attribute",
                                     parseExp, e);
@@ -103,7 +103,7 @@
                 try {
                     try {
                         ignoreMissing = Boolean.valueOf(
-                                ignoreMissingExp.evalToBoolean(template.getConfiguration()));
+                                ignoreMissingExp.evalToBoolean(unboundTemplate.getConfiguration()));
                     } catch(NonBooleanException e) {
                         throw new ParseException("Expected a boolean as the value of the \"ignore_missing\" attribute",
                                 ignoreMissingExp, e);
@@ -122,7 +122,7 @@
         final String includedTemplateName = includedTemplateNameExp.evalAndCoerceToString(env);
         final String fullIncludedTemplateName;
         try {
-            fullIncludedTemplateName = env.toFullTemplateName(getTemplate().getName(), includedTemplateName);
+            fullIncludedTemplateName = env.toFullTemplateName(env.getCurrentTemplate().getName(), includedTemplateName);
         } catch (MalformedTemplateNameException e) {
             throw new _MiscTemplateException(e, env, new Object[] {
                     "Malformed template name ", new _DelayedJQuote(e.getTemplateName()), ":\n",
diff --git a/src/main/java/freemarker/core/LibraryLoad.java b/src/main/java/freemarker/core/LibraryLoad.java
index 4825a53..5259029 100644
--- a/src/main/java/freemarker/core/LibraryLoad.java
+++ b/src/main/java/freemarker/core/LibraryLoad.java
@@ -30,15 +30,15 @@
  */
 public final class LibraryLoad extends TemplateElement {
 
-    private Expression importedTemplateNameExp;
-    private String namespace;
+    private final Expression importedTemplateNameExp;
+    private final String namespace;
 
     /**
-     * @param template the template that this <tt>Include</tt> is a part of.
+     * @param unboundTemplate the template that this <tt>Include</tt> is a part of.
      * @param templateName the name of the template to be included.
      * @param namespace the namespace to assign this library to
      */
-    LibraryLoad(Template template,
+    LibraryLoad(UnboundTemplate unboundTemplate,
             Expression templateName,
             String namespace)
     {
@@ -50,7 +50,7 @@
         final String importedTemplateName = importedTemplateNameExp.evalAndCoerceToString(env);
         final String fullImportedTemplateName;
         try {
-            fullImportedTemplateName = env.toFullTemplateName(getTemplate().getName(), importedTemplateName);
+            fullImportedTemplateName = env.toFullTemplateName(env.getCurrentTemplate().getName(), importedTemplateName);
         } catch (MalformedTemplateNameException e) {
             throw new _MiscTemplateException(e, env, new Object[] {
                     "Malformed template name ", new _DelayedJQuote(e.getTemplateName()), ":\n",
diff --git a/src/main/java/freemarker/core/Macro.java b/src/main/java/freemarker/core/Macro.java
index e7a2175..069145b 100644
--- a/src/main/java/freemarker/core/Macro.java
+++ b/src/main/java/freemarker/core/Macro.java
@@ -16,312 +16,34 @@
 
 package freemarker.core;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-
-import freemarker.template.TemplateException;
 import freemarker.template.TemplateModel;
-import freemarker.template.TemplateModelException;
-import freemarker.template.TemplateModelIterator;
 
 /**
- * An element representing a macro declaration.
+ * Exist for backward compatibility only; it has represented a macro or function declaration in the AST. The current
+ * representation isn't a public class, but is {@code insteanceof} this for backward compatibility.
+ * 
+ * <p>
+ * Historical note: This class exists for bacward compatibility with 2.3. 2.4 has introduced {@link UnboundTemplate}-s,
+ * thus, the definition of a callable and the runtime callable value has become to two different things:
+ * {@link UnboundCallable} and {@link BoundCallable}. Both extends this class for backward compatibility.
+ * 
+ * @see UnboundCallable
+ * @see BoundCallable
  * 
  * @deprecated Subject to be changed or renamed any time; no "stable" replacement exists yet.
  */
-public final class Macro extends TemplateElement implements TemplateModel {
-
-    static final Macro DO_NOTHING_MACRO = new Macro(".pass", 
-            Collections.EMPTY_LIST, 
-            Collections.EMPTY_MAP,
-            null, false,
-            TextBlock.EMPTY_BLOCK);
+public abstract class Macro extends TemplateElement implements TemplateModel {
     
-    final static int TYPE_MACRO = 0;
-    final static int TYPE_FUNCTION = 1;
-    
-    private final String name;
-    private final String[] paramNames;
-    private final Map paramDefaults;
-    private final String catchAllParamName;
-    private final boolean function;
+    // Not public
+    Macro() { }
 
-    Macro(String name, List argumentNames, Map args, 
-            String catchAllParamName, boolean function,
-            TemplateElement nestedBlock) 
-    {
-        this.name = name;
-        this.paramNames = (String[])argumentNames.toArray(
-                new String[argumentNames.size()]);
-        this.paramDefaults = args;
-        
-        this.function = function;
-        this.catchAllParamName = catchAllParamName; 
-        
-        this.nestedBlock = nestedBlock;
-    }
+    public abstract String getCatchAll();
 
-    public String getCatchAll() {
-        return catchAllParamName;
-    }
-    
-    public String[] getArgumentNames() {
-        return (String[])paramNames.clone();
-    }
+    public abstract String[] getArgumentNames();
 
-    String[] getArgumentNamesInternal() {
-        return paramNames;
-    }
+    public abstract String getName();
 
-    boolean hasArgNamed(String name) {
-        return paramDefaults.containsKey(name);
-    }
-    
-    public String getName() {
-        return name;
-    }
-
-    void accept(Environment env) {
-        env.visitMacroDef(this);
-    }
-
-    protected String dump(boolean canonical) {
-        StringBuffer sb = new StringBuffer();
-        if (canonical) sb.append('<');
-        sb.append(getNodeTypeSymbol());
-        sb.append(' ');
-        sb.append(_CoreStringUtils.toFTLTopLevelTragetIdentifier(name));
-        sb.append(function ? '(' : ' ');
-        int argCnt = paramNames.length;
-        for (int i = 0; i < argCnt; i++) {
-            if (i != 0) {
-                if (function) {
-                    sb.append(", ");
-                } else {
-                    sb.append(' ');
-                }
-            }
-            String argName = paramNames[i];
-            sb.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(argName));
-            if (paramDefaults != null && paramDefaults.get(argName) != null) {
-                sb.append('=');
-                Expression defaultExpr = (Expression) paramDefaults.get(argName);
-                if (function) {
-                    sb.append(defaultExpr.getCanonicalForm());
-                } else {
-                    MessageUtil.appendExpressionAsUntearable(sb, defaultExpr);
-                }
-            }
-        }
-        if (catchAllParamName != null) {
-            if (argCnt != 0) sb.append(", ");
-            sb.append(catchAllParamName);
-            sb.append("...");
-        }
-        if (function) sb.append(')');
-        if (canonical) {
-            sb.append('>');
-            if (nestedBlock != null) {
-                sb.append(nestedBlock.getCanonicalForm());
-            }
-            sb.append("</").append(getNodeTypeSymbol()).append('>');
-        }
-        return sb.toString();
-    }
-    
-    String getNodeTypeSymbol() {
-        return function ? "#function" : "#macro";
-    }
-    
-    boolean isShownInStackTrace() {
-        return false;
-    }
-    
-    public boolean isFunction() {
-        return function;
-    }
-
-    class Context implements LocalContext {
-        final Environment.Namespace localVars; 
-        final TemplateElement nestedContent;
-        final Environment.Namespace nestedContentNamespace;
-        final List nestedContentParameterNames;
-        final ArrayList prevLocalContextStack;
-        final Context prevMacroContext;
-        
-        Context(Environment env, 
-                TemplateElement nestedContent,
-                List nestedContentParameterNames) 
-        {
-            this.localVars = env.new Namespace();
-            this.nestedContent = nestedContent;
-            this.nestedContentNamespace = env.getCurrentNamespace();
-            this.nestedContentParameterNames = nestedContentParameterNames;
-            this.prevLocalContextStack = env.getLocalContextStack();
-            this.prevMacroContext = env.getCurrentMacroContext();
-        }
-                
-        
-        Macro getMacro() {
-            return Macro.this;
-        }
-
-        void runMacro(Environment env) throws TemplateException, IOException {
-            sanityCheck(env);
-            // Set default values for unspecified parameters
-            if (nestedBlock != null) {
-                env.visit(nestedBlock);
-            }
-        }
-
-        // Set default parameters, check if all the required parameters are defined.
-        void sanityCheck(Environment env) throws TemplateException {
-            boolean resolvedAnArg, hasUnresolvedArg;
-            Expression firstUnresolvedExpression;
-            InvalidReferenceException firstReferenceException;
-            do {
-                firstUnresolvedExpression = null;
-                firstReferenceException = null;
-                resolvedAnArg = hasUnresolvedArg = false;
-                for(int i = 0; i < paramNames.length; ++i) {
-                    String argName = paramNames[i];
-                    if(localVars.get(argName) == null) {
-                        Expression valueExp = (Expression) paramDefaults.get(argName);
-                        if (valueExp != null) {
-                            try {
-                                TemplateModel tm = valueExp.eval(env);
-                                if(tm == null) {
-                                    if(!hasUnresolvedArg) {
-                                        firstUnresolvedExpression = valueExp;
-                                        hasUnresolvedArg = true;
-                                    }
-                                }
-                                else {
-                                    localVars.put(argName, tm);
-                                    resolvedAnArg = true;
-                                }
-                            }
-                            catch(InvalidReferenceException e) {
-                                if(!hasUnresolvedArg) {
-                                    hasUnresolvedArg = true;
-                                    firstReferenceException = e;
-                                }
-                            }
-                        }
-                        else if (!env.isClassicCompatible()) {
-                            boolean argWasSpecified = localVars.containsKey(argName);
-                            throw new _MiscTemplateException(env,
-                                    new _ErrorDescriptionBuilder(new Object[] {
-                                            "When calling macro ", new _DelayedJQuote(name), 
-                                            ", required parameter ", new _DelayedJQuote(argName),
-                                            " (parameter #", new Integer(i + 1), ") was ", 
-                                            (argWasSpecified
-                                                    ? "specified, but had null/missing value."
-                                                    : "not specified.") 
-                                    }).tip(argWasSpecified
-                                            ? new Object[] {
-                                                    "If the parameter value expression on the caller side is known to "
-                                                    + "be legally null/missing, you may want to specify a default "
-                                                    + "value for it with the \"!\" operator, like "
-                                                    + "paramValue!defaultValue." }
-                                            : new Object[] { 
-                                                    "If the omission was deliberate, you may consider making the "
-                                                    + "parameter optional in the macro by specifying a default value "
-                                                    + "for it, like ", "<#macro macroName paramName=defaultExpr>", ")" }
-                                            ));
-                        }
-                    }
-                }
-            }
-            while(resolvedAnArg && hasUnresolvedArg);
-            if(hasUnresolvedArg) {
-                if(firstReferenceException != null) {
-                    throw firstReferenceException;
-                } else if (!env.isClassicCompatible()) {
-                    throw InvalidReferenceException.getInstance(firstUnresolvedExpression, env);
-                }
-            }
-        }
-
-        /**
-         * @return the local variable of the given name
-         * or null if it doesn't exist.
-         */ 
-        public TemplateModel getLocalVariable(String name) throws TemplateModelException {
-             return localVars.get(name);
-        }
-
-        Environment.Namespace getLocals() {
-            return localVars;
-        }
-        
-        /**
-         * Set a local variable in this macro 
-         */
-        void setLocalVar(String name, TemplateModel var) {
-            localVars.put(name, var);
-        }
-
-        public Collection getLocalVariableNames() throws TemplateModelException {
-            HashSet result = new HashSet();
-            for (TemplateModelIterator it = localVars.keys().iterator(); it.hasNext();) {
-                result.add(it.next().toString());
-            }
-            return result;
-        }
-    }
-
-    int getParameterCount() {
-        return 1/*name*/ + paramNames.length * 2/*name=default*/ + 1/*catchAll*/ + 1/*type*/;
-    }
-
-    Object getParameterValue(int idx) {
-        if (idx == 0) {
-            return name;
-        } else {
-            final int argDescsEnd = paramNames.length * 2 + 1;
-            if (idx < argDescsEnd) {
-                String paramName = paramNames[(idx - 1) / 2];
-                if (idx % 2 != 0) {
-                    return paramName;
-                } else {
-                    return paramDefaults.get(paramName);
-                }
-            } else if (idx == argDescsEnd) {
-                return catchAllParamName;
-            } else if (idx == argDescsEnd + 1) {
-                return new Integer(function ? TYPE_FUNCTION : TYPE_MACRO);
-            } else {
-                throw new IndexOutOfBoundsException();
-            }
-        }
-    }
-
-    ParameterRole getParameterRole(int idx) {
-        if (idx == 0) {
-            return ParameterRole.ASSIGNMENT_TARGET;
-        } else {
-            final int argDescsEnd = paramNames.length * 2 + 1;
-            if (idx < argDescsEnd) {
-                if (idx % 2 != 0) {
-                    return ParameterRole.PARAMETER_NAME;
-                } else {
-                    return ParameterRole.PARAMETER_DEFAULT;
-                }
-            } else if (idx == argDescsEnd) {
-                return ParameterRole.CATCH_ALL_PARAMETER_NAME;
-            } else if (idx == argDescsEnd + 1) {
-                return ParameterRole.AST_NODE_SUBTYPE;
-            } else {
-                throw new IndexOutOfBoundsException();
-            }
-        }
-    }
+    public abstract boolean isFunction();
 
     boolean isNestedBlockRepeater() {
         // Because of recursive calls
diff --git a/src/main/java/freemarker/core/MessageUtil.java b/src/main/java/freemarker/core/MessageUtil.java
index a1f1a46..2c84912 100644
--- a/src/main/java/freemarker/core/MessageUtil.java
+++ b/src/main/java/freemarker/core/MessageUtil.java
@@ -18,7 +18,6 @@
 
 import java.util.ArrayList;
 
-import freemarker.template.Template;
 import freemarker.template.TemplateException;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
@@ -49,37 +48,39 @@
     // Can't be instantiated
     private MessageUtil() { }
         
-    static String formatLocationForSimpleParsingError(Template template, int line, int column) {
-        return formatLocation("in", template, line, column);
+    static String formatLocationForSimpleParsingError(UnboundTemplate unboundTemplate, int line, int column) {
+        return formatLocation("in", unboundTemplate, line, column);
     }
 
     static String formatLocationForSimpleParsingError(String templateSourceName, int line, int column) {
         return formatLocation("in", templateSourceName, line, column);
     }
 
-    static String formatLocationForDependentParsingError(Template template, int line, int column) {
-        return formatLocation("on", template, line, column);
+    static String formatLocationForDependentParsingError(UnboundTemplate unboundTemplate, int line, int column) {
+        return formatLocation("on", unboundTemplate, line, column);
     }
 
     static String formatLocationForDependentParsingError(String templateSourceName, int line, int column) {
         return formatLocation("on", templateSourceName, line, column);
     }
 
-    static String formatLocationForEvaluationError(Template template, int line, int column) {
-        return formatLocation("at", template, line, column);
+    static String formatLocationForEvaluationError(UnboundTemplate unboundTemplate, int line, int column) {
+        return formatLocation("at", unboundTemplate, line, column);
     }
 
     static String formatLocationForEvaluationError(Macro macro, int line, int column) {
-        Template t = macro.getTemplate();
-        return formatLocation("at", t != null ? t.getSourceName() : null, macro.getName(), macro.isFunction(), line, column);
+        UnboundTemplate t = macro.getUnboundTemplate();
+        return formatLocation("at",
+                t != null ? t.getSourceName() : null, macro.getName(), macro.isFunction(), line, column);
     }
     
     static String formatLocationForEvaluationError(String templateSourceName, int line, int column) {
         return formatLocation("at", templateSourceName, line, column);
     }
 
-    private static String formatLocation(String preposition, Template template, int line, int column) {
-        return formatLocation(preposition, template != null ? template.getSourceName() : null, line, column);
+    private static String formatLocation(String preposition, UnboundTemplate unboundTemplate, int line, int column) {
+        return formatLocation(preposition,
+                unboundTemplate != null ? unboundTemplate.getSourceName() : null, line, column);
     }
     
     private static String formatLocation(String preposition, String templateSourceName, int line, int column) {
diff --git a/src/main/java/freemarker/core/MethodCall.java b/src/main/java/freemarker/core/MethodCall.java
index 53d5203..6472a7d 100644
--- a/src/main/java/freemarker/core/MethodCall.java
+++ b/src/main/java/freemarker/core/MethodCall.java
@@ -62,16 +62,17 @@
             Object result = targetMethod.exec(argumentStrings);
             return env.getObjectWrapper().wrap(result);
         }
-        else if (targetModel instanceof Macro) {
-            Macro func = (Macro) targetModel;
+        else if (targetModel instanceof BoundCallable) {
+            final BoundCallable boundFunc = (BoundCallable) targetModel;
+            final Macro unboundFunc = boundFunc.getUnboundCallable();
             env.setLastReturnValue(null);
-            if (!func.isFunction()) {
+            if (!unboundFunc.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);
+                env.invoke(boundFunc, null, arguments.items, null, null);
             } catch (IOException e) {
                 // Should not occur
                 throw new TemplateException("Unexpected exception during function execution", e, env);
diff --git a/src/main/java/freemarker/core/NewBI.java b/src/main/java/freemarker/core/NewBI.java
index 3c23d16..1b72062 100644
--- a/src/main/java/freemarker/core/NewBI.java
+++ b/src/main/java/freemarker/core/NewBI.java
@@ -20,7 +20,6 @@
 
 import freemarker.ext.beans.BeansWrapper;
 import freemarker.template.ObjectWrapper;
-import freemarker.template.Template;
 import freemarker.template.TemplateException;
 import freemarker.template.TemplateMethodModelEx;
 import freemarker.template.TemplateModel;
@@ -46,7 +45,7 @@
     TemplateModel _eval(Environment env)
             throws TemplateException 
     {
-        return new ConstructorFunction(target.evalAndCoerceToString(env), env, target.getTemplate());
+        return new ConstructorFunction(target.evalAndCoerceToString(env), env);
     }
 
     class ConstructorFunction implements TemplateMethodModelEx {
@@ -54,9 +53,9 @@
         private final Class cl;
         private final Environment env;
         
-        public ConstructorFunction(String classname, Environment env, Template template) throws TemplateException {
+        public ConstructorFunction(String classname, Environment env) throws TemplateException {
             this.env = env;
-            cl = env.getNewBuiltinClassResolver().resolve(classname, env, template);
+            cl = env.getNewBuiltinClassResolver().resolve(classname, env, env.getCurrentTemplate());
             if (!TemplateModel.class.isAssignableFrom(cl)) {
                 throw new _MiscTemplateException(NewBI.this, env, new Object[] {
                         "Class ", cl.getName(), " does not implement freemarker.template.TemplateModel" });
diff --git a/src/main/java/freemarker/core/NonUserDefinedDirectiveLikeException.java b/src/main/java/freemarker/core/NonUserDefinedDirectiveLikeException.java
index 263944b..a41fd8c 100644
--- a/src/main/java/freemarker/core/NonUserDefinedDirectiveLikeException.java
+++ b/src/main/java/freemarker/core/NonUserDefinedDirectiveLikeException.java
@@ -29,7 +29,7 @@
 class NonUserDefinedDirectiveLikeException extends UnexpectedTypeException {
 
     private static final Class[] EXPECTED_TYPES = new Class[] {
-        TemplateDirectiveModel.class, TemplateTransformModel.class, Macro.class };
+        TemplateDirectiveModel.class, TemplateTransformModel.class, BoundCallable.class };
     
     public NonUserDefinedDirectiveLikeException(Environment env) {
         super(env, "Expecting user-defined directive, transform or macro value here");
diff --git a/src/main/java/freemarker/core/ParseException.java b/src/main/java/freemarker/core/ParseException.java
index 9b5e1f9..5b99d4d 100644
--- a/src/main/java/freemarker/core/ParseException.java
+++ b/src/main/java/freemarker/core/ParseException.java
@@ -129,14 +129,16 @@
     }
 
     /**
+     * @deprecated Use {@link #ParseException(String, UnboundTemplate, int, int, int, int)} instead.
      * @since 2.3.21
-     */
+     /
     public ParseException(String description, Template template,
             int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber) {
         this(description, template, lineNumber, columnNumber, endLineNumber, endColumnNumber, null);      
     }
 
     /**
+     * @deprecated Use {@link #ParseException(String, UnboundTemplate, int, int, int, int, Throwable)} instead.
      * @since 2.3.21
      */
     public ParseException(String description, Template template,
@@ -150,7 +152,7 @@
     }
     
     /**
-     * @deprecated Use {@link #ParseException(String, Template, int, int, int, int)} instead, as IDE-s need the end
+     * @deprecated Use {@link #ParseException(String, UnboundTemplate, int, int, int, int)} instead, as IDE-s need the end
      * position of the error too.
      * @since 2.3.20
      */
@@ -159,8 +161,8 @@
     }
 
     /**
-     * @deprecated Use {@link #ParseException(String, Template, int, int, int, int, Throwable)} instead, as IDE-s need
-     * the end position of the error too.
+     * @deprecated Use {@link #ParseException(String, UnboundTemplate, int, int, int, int, Throwable)} instead, as
+     * IDE-s need the end position of the error too.
      * @since 2.3.20
      */
     public ParseException(String description, Template template, int lineNumber, int columnNumber, Throwable cause) {
@@ -172,6 +174,7 @@
     }
 
     /**
+     * @deprecated Use {@link #ParseException(String, UnboundTemplate, Token)} instead.
      * @since 2.3.20
      */
     public ParseException(String description, Template template, Token tk) {
@@ -179,6 +182,7 @@
     }
 
     /**
+     * @deprecated Use {@link #ParseException(String, UnboundTemplate, Token, Throwable)} instead.
      * @since 2.3.20
      */
     public ParseException(String description, Template template, Token tk, Throwable cause) {
@@ -190,6 +194,45 @@
     }
 
     /**
+     * @since 2.4.0
+     */
+    public ParseException(String description, UnboundTemplate unboundTemplate,
+            int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber) {
+        this(description, unboundTemplate, lineNumber, columnNumber, endLineNumber, endColumnNumber, null);      
+    }
+
+    /**
+     * @since 2.4.0
+     */
+    public ParseException(String description, UnboundTemplate unboundTemplate,
+            int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber,
+            Throwable cause) {
+        this(description,
+                unboundTemplate == null ? null : unboundTemplate.getSourceName(),
+                        lineNumber, columnNumber,
+                        endLineNumber, endColumnNumber,
+                        cause);      
+    }
+    
+    /**
+     * @since 2.4.0
+     */
+    public ParseException(String description, UnboundTemplate unboundTemplate, Token tk) {
+        this(description, unboundTemplate, tk, null);
+    }
+
+    /**
+     * @since 2.4.0
+     */
+    public ParseException(String description, UnboundTemplate unboundTemplate, Token tk, Throwable cause) {
+        this(description,
+                unboundTemplate == null ? null : unboundTemplate.getSourceName(),
+                        tk.beginLine, tk.beginColumn,
+                        tk.endLine, tk.endColumn,
+                        cause);
+    }
+    
+    /**
      * @since 2.3.20
      */
     public ParseException(String description, TemplateObject tobj) {
@@ -201,7 +244,7 @@
      */
     public ParseException(String description, TemplateObject tobj, Throwable cause) {
         this(description,
-                tobj.getTemplate() == null ? null : tobj.getTemplate().getSourceName(),
+                tobj.getUnboundTemplate() == null ? null : tobj.getUnboundTemplate().getSourceName(),
                         tobj.beginLine, tobj.beginColumn,
                         tobj.endLine, tobj.endColumn,
                         cause);
diff --git a/src/main/java/freemarker/core/PropertySetting.java b/src/main/java/freemarker/core/PropertySetting.java
index 6f62365..6b1d110 100644
--- a/src/main/java/freemarker/core/PropertySetting.java
+++ b/src/main/java/freemarker/core/PropertySetting.java
@@ -18,7 +18,6 @@
 
 import java.util.Arrays;
 
-import freemarker.template.Template;
 import freemarker.template.TemplateBooleanModel;
 import freemarker.template.TemplateException;
 import freemarker.template.TemplateModel;
@@ -54,11 +53,11 @@
         this.value = value;
     }
 
-    void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine)
+    void setLocation(UnboundTemplate unboundTemplate, int beginColumn, int beginLine, int endColumn, int endLine)
     throws
         ParseException
     {
-        super.setLocation(template, beginColumn, beginLine, endColumn, endLine);
+        super.setLocation(unboundTemplate, beginColumn, beginLine, endColumn, endLine);
         
         if (Arrays.binarySearch(SETTING_NAMES, key) < 0) {
             StringBuffer sb = new StringBuffer();
@@ -69,8 +68,8 @@
                 sb.append(" Supporting camelCase setting names is planned for FreeMarker 2.4.0; check if an update is "
                             + "available, and if it indeed supports camel case. "
                             + "Until that, use \"").append(underscoredName).append("\".");
-            } else if (((Configurable) template).getSettingNames().contains(key)
-                    || ((Configurable) template).getSettingNames().contains(underscoredName)) {
+            } else if (Configurable.getConfigurableSettingNames().contains(key)
+                    || Configurable.getConfigurableSettingNames().contains(underscoredName)) {
                 sb.append(" The setting name is recognized, but changing this setting in a template isn't supported.");                
             } else {
                 sb.append(" The allowed setting names are: ");
@@ -83,7 +82,7 @@
             }
             throw new ParseException(
                     sb.toString(),
-                    template, beginLine, beginColumn, endLine, endColumn);
+                    unboundTemplate, beginLine, beginColumn, endLine, endColumn);
         }
     }
 
diff --git a/src/main/java/freemarker/core/StringLiteral.java b/src/main/java/freemarker/core/StringLiteral.java
index 92c86b7..6efde02 100644
--- a/src/main/java/freemarker/core/StringLiteral.java
+++ b/src/main/java/freemarker/core/StringLiteral.java
@@ -41,12 +41,12 @@
             FMParserTokenManager token_source = new FMParserTokenManager(scs);
             token_source.onlyTextOutput = true;
             FMParser parser = new FMParser(token_source);
-            parser.setTemplate(getTemplate());
+            parser.setTemplate(getUnboundTemplate());
             try {
                 dynamicValue = parser.FreeMarkerText();
             }
             catch(ParseException e) {
-                e.setTemplateName(getTemplate().getSourceName());
+                e.setTemplateName(getUnboundTemplate().getSourceName());
                 throw e;
             }
             this.constantValue = null;
diff --git a/src/main/java/freemarker/core/TemplateObject.java b/src/main/java/freemarker/core/TemplateObject.java
index ffb4e93..6b7ec0a 100644
--- a/src/main/java/freemarker/core/TemplateObject.java
+++ b/src/main/java/freemarker/core/TemplateObject.java
@@ -16,7 +16,6 @@
 
 package freemarker.core;
 
-import freemarker.template.Template;
 
 /**
  * <b>Internal API - subject to change:</b> Represent a node in the parsed template (either a {@link Expression} or a
@@ -30,7 +29,7 @@
  */
 public abstract class TemplateObject {
     
-    private Template template;
+    private UnboundTemplate unboundTemplate;
     int beginColumn, beginLine, endColumn, endLine;
     
     /** This is needed for an ?eval hack; the expression AST nodes will be the descendants of the template, however,
@@ -38,93 +37,116 @@
      *  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)
+    final void setLocation(UnboundTemplate unboundTemplate, Token begin, Token end)
     throws
         ParseException
     {
-        setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+        setLocation(unboundTemplate, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
     }
 
-    final void setLocation(Template template, Token begin, TemplateObject end)
+    final void setLocation(UnboundTemplate unboundTemplate, Token begin, TemplateObject end)
     throws
         ParseException
     {
-        setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+        setLocation(unboundTemplate, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
     }
 
-    final void setLocation(Template template, TemplateObject begin, Token end)
+    final void setLocation(UnboundTemplate unboundTemplate, TemplateObject begin, Token end)
     throws
         ParseException
     {
-        setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+        setLocation(unboundTemplate, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
     }
 
-    final void setLocation(Template template, TemplateObject begin, TemplateObject end)
+    final void setLocation(UnboundTemplate unboundTemplate, TemplateObject begin, TemplateObject end)
     throws
         ParseException
     {
-        setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
+        setLocation(unboundTemplate, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine);
     }
 
-    void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine)
+    void setLocation(UnboundTemplate unboundTemplate, int beginColumn, int beginLine, int endColumn, int endLine)
     throws
         ParseException
     {
-        this.template = template;
+        this.unboundTemplate = unboundTemplate;
         this.beginColumn = beginColumn;
         this.beginLine = beginLine;
         this.endColumn = endColumn;
         this.endLine = endLine;
     }
     
+    /**
+     * <b>Internal API - subject to change</b>
+     */
     public final int getBeginColumn() {
         return beginColumn;
     }
 
+    /**
+     * <b>Internal API - subject to change</b>
+     */
     public final int getBeginLine() {
         return beginLine;
     }
 
+    /**
+     * <b>Internal API - subject to change</b>
+     */
     public final int getEndColumn() {
         return endColumn;
     }
 
+    /**
+     * <b>Internal API - subject to change</b>
+     */
     public final int getEndLine() {
         return endLine;
     }
 
     /**
+     * <b>Internal API - subject to change</b>;
      * Returns a string that indicates
      * where in the template source, this object is.
      */
     public String getStartLocation() {
-        return MessageUtil.formatLocationForEvaluationError(template, beginLine, beginColumn);
+        return MessageUtil.formatLocationForEvaluationError(unboundTemplate, beginLine, beginColumn);
     }
 
     /**
-     * As of 2.3.20. the same as {@link #getStartLocation}. Meant to be used where there's a risk of XSS
+     * <b>Internal API - subject to change</b>
+     * 
+     * <p>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();
     }
 
+    /**
+     * <b>Internal API - subject to change</b>
+     */
     public String getEndLocation() {
-        return MessageUtil.formatLocationForEvaluationError(template, endLine, endColumn);
+        return MessageUtil.formatLocationForEvaluationError(unboundTemplate, endLine, endColumn);
     }
 
     /**
-     * As of 2.3.20. the same as {@link #getEndLocation}. Meant to be used where there's a risk of XSS
+     * <b>Internal API - subject to change</b>
+     * 
+     * <p>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();
     }
     
+    /**
+     * <b>Internal API - subject to change</b>
+     */
     public final String getSource() {
         String s;
-        if (template != null) {
-            s = template.getSource(beginColumn, beginLine, endColumn, endLine);
+        if (unboundTemplate != null) {
+            s = unboundTemplate.getSource(beginColumn, beginLine, endColumn, endLine);
         } else {
             s = null;
         }
@@ -144,8 +166,10 @@
     }
 
     /**
-     * @return whether the point in the template file specified by the 
-     * column and line numbers is contained within this template object.
+     * <b>Internal API - subject to change</b>
+     * 
+     * @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) {
@@ -165,15 +189,17 @@
     }
 
     /**
-     * @deprecated This method will be removed in FreeMarker 2.4 because of architectural changes!
+     * <b>Internal API - subject to change</b>
+     * 
+     * @since 2.4.0
      */
-    public Template getTemplate() {
-        return template;
+    public UnboundTemplate getUnboundTemplate() {
+        return unboundTemplate;
     }
-    
+
     TemplateObject copyLocationFrom(TemplateObject from)
     {
-        template = from.template;
+        unboundTemplate = from.unboundTemplate;
         beginColumn = from.beginColumn;
         beginLine = from.beginLine;
         endColumn = from.endColumn;
@@ -182,8 +208,9 @@
     }    
 
     /**
-     * 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.
+     * <b>Internal API - subject to change</b>; 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 TemplateElement#getDescription()
      * @see #getNodeTypeSymbol()
diff --git a/src/main/java/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java b/src/main/java/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java
index 990dca7..5709732 100644
--- a/src/main/java/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java
+++ b/src/main/java/freemarker/core/ThreadInterruptionSupportTemplatePostProcessor.java
@@ -87,7 +87,7 @@
                         nestedMixedC = (MixedContent) nestedBlock;
                     } else {
                         nestedMixedC = new MixedContent();
-                        nestedMixedC.setLocation(te.getTemplate(), 0, 0, 0, 0);
+                        nestedMixedC.setLocation(te.getUnboundTemplate(), 0, 0, 0, 0);
                         nestedMixedC.parent = te;
                         nestedBlock.parent = nestedMixedC;
                         nestedMixedC.addElement(nestedBlock);
@@ -108,7 +108,7 @@
     static class ThreadInterruptionCheck extends TemplateElement {
         
         private ThreadInterruptionCheck(TemplateElement te) throws ParseException {
-            setLocation(te.getTemplate(), 0, 0, 0, 0);
+            setLocation(te.getUnboundTemplate(), 0, 0, 0, 0);
             parent = te;
         }
 
diff --git a/src/main/java/freemarker/core/TokenMgrError.java b/src/main/java/freemarker/core/TokenMgrError.java
index 34354ee..0ee0e10 100644
--- a/src/main/java/freemarker/core/TokenMgrError.java
+++ b/src/main/java/freemarker/core/TokenMgrError.java
@@ -18,6 +18,8 @@
 
 import freemarker.template.Template;
 
+
+
 /**
  * Exception thrown on lower (lexical) level parsing errors. Shouldn't reach normal FreeMarker users, as FreeMarker
  * usually catches this and wraps it into a {@link ParseException}.
@@ -238,9 +240,19 @@
        return detail;
    }
 
+   /**
+    * @deprecated Use {@link #toParseException(UnboundTemplate)} instead. 
+    */
    public ParseException toParseException(Template template) {
+       return toParseException(template.getUnboundTemplate());
+   }
+   
+   /**
+    * @since 2.4.0
+    */
+   public ParseException toParseException(UnboundTemplate unboundTemplate) {
        return new ParseException(getDetail(),
-               template,
+               unboundTemplate,
                getLineNumber() != null ? getLineNumber().intValue() : 0,
                getColumnNumber() != null ? getColumnNumber().intValue() : 0,
                getEndLineNumber() != null ? getEndLineNumber().intValue() : 0,
diff --git a/src/main/java/freemarker/core/UnboundCallable.java b/src/main/java/freemarker/core/UnboundCallable.java
new file mode 100644
index 0000000..902e823
--- /dev/null
+++ b/src/main/java/freemarker/core/UnboundCallable.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package freemarker.core;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import freemarker.template.TemplateModel;
+
+/**
+ * Represents the definition of a macro or function in the AST. For understanding related concepts more, see
+ * {@link BoundCallable}.
+ * 
+ * <p>
+ * Historical note: Prior to 2.4, the two concepts ({@link UnboundCallable} and {@link BoundCallable}) were these same,
+ * represented by {@link Macro}, which still exists due to backward compatibility constraints, but now is abstract and
+ * is implemented by this class. This class should not implement {@link TemplateModel} (which it does, because
+ * {@link Macro} implements it), but it had to, for backward compatibility.
+ * 
+ * @see BoundCallable
+ * 
+ * @since 2.4.0
+ */
+class UnboundCallable extends Macro {
+
+    static final UnboundCallable NO_OP_MACRO = new UnboundCallable(".pass", 
+            Collections.EMPTY_LIST, 
+            Collections.EMPTY_MAP,
+            null, false,
+            TextBlock.EMPTY_BLOCK);
+    
+    final static int TYPE_MACRO = 0;
+    final static int TYPE_FUNCTION = 1;
+    
+    private final String name;
+    private final String[] paramNames;
+    private final Map paramDefaults;
+    private final String catchAllParamName;
+    private final boolean function;
+
+    UnboundCallable(String name, List argumentNames, Map args, 
+            String catchAllParamName, boolean function,
+            TemplateElement nestedBlock) 
+    {
+        this.name = name;
+        this.paramNames = (String[])argumentNames.toArray(
+                new String[argumentNames.size()]);
+        this.paramDefaults = args;
+        
+        this.function = function;
+        this.catchAllParamName = catchAllParamName; 
+        
+        this.nestedBlock = nestedBlock;
+    }
+    
+    String[] getParamNames() {
+        return paramNames;
+    }
+    
+    Map getParamDefaults() {
+        return paramDefaults;
+    }
+
+    public String getCatchAll() {
+        return catchAllParamName;
+    }
+    
+    public String[] getArgumentNames() {
+        return (String[])paramNames.clone();
+    }
+
+    String[] getArgumentNamesInternal() {
+        return paramNames;
+    }
+
+    boolean hasArgNamed(String name) {
+        return paramDefaults.containsKey(name);
+    }
+    
+    public String getName() {
+        return name;
+    }
+
+    void accept(Environment env) {
+        env.visitCallableDefinition(this);
+    }
+
+    protected String dump(boolean canonical) {
+        StringBuffer sb = new StringBuffer();
+        if (canonical) sb.append('<');
+        sb.append(getNodeTypeSymbol());
+        sb.append(' ');
+        sb.append(_CoreStringUtils.toFTLTopLevelTragetIdentifier(name));
+        sb.append(function ? '(' : ' ');
+        int argCnt = paramNames.length;
+        for (int i = 0; i < argCnt; i++) {
+            if (i != 0) {
+                if (function) {
+                    sb.append(", ");
+                } else {
+                    sb.append(' ');
+                }
+            }
+            String argName = paramNames[i];
+            sb.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(argName));
+            if (paramDefaults != null && paramDefaults.get(argName) != null) {
+                sb.append('=');
+                Expression defaultExpr = (Expression) paramDefaults.get(argName);
+                if (function) {
+                    sb.append(defaultExpr.getCanonicalForm());
+                } else {
+                    MessageUtil.appendExpressionAsUntearable(sb, defaultExpr);
+                }
+            }
+        }
+        if (catchAllParamName != null) {
+            if (argCnt != 0) sb.append(", ");
+            sb.append(catchAllParamName);
+            sb.append("...");
+        }
+        if (function) sb.append(')');
+        if (canonical) {
+            sb.append('>');
+            if (nestedBlock != null) {
+                sb.append(nestedBlock.getCanonicalForm());
+            }
+            sb.append("</").append(getNodeTypeSymbol()).append('>');
+        }
+        return sb.toString();
+    }
+    
+    String getNodeTypeSymbol() {
+        return function ? "#function" : "#macro";
+    }
+    
+    boolean isShownInStackTrace() {
+        return false;
+    }
+    
+    public boolean isFunction() {
+        return function;
+    }
+
+    int getParameterCount() {
+        return 1/*name*/ + paramNames.length * 2/*name=default*/ + 1/*catchAll*/ + 1/*type*/;
+    }
+
+    Object getParameterValue(int idx) {
+        if (idx == 0) {
+            return name;
+        } else {
+            final int argDescsEnd = paramNames.length * 2 + 1;
+            if (idx < argDescsEnd) {
+                String paramName = paramNames[(idx - 1) / 2];
+                if (idx % 2 != 0) {
+                    return paramName;
+                } else {
+                    return paramDefaults.get(paramName);
+                }
+            } else if (idx == argDescsEnd) {
+                return catchAllParamName;
+            } else if (idx == argDescsEnd + 1) {
+                return new Integer(function ? TYPE_FUNCTION : TYPE_MACRO);
+            } else {
+                throw new IndexOutOfBoundsException();
+            }
+        }
+    }
+
+    ParameterRole getParameterRole(int idx) {
+        if (idx == 0) {
+            return ParameterRole.ASSIGNMENT_TARGET;
+        } else {
+            final int argDescsEnd = paramNames.length * 2 + 1;
+            if (idx < argDescsEnd) {
+                if (idx % 2 != 0) {
+                    return ParameterRole.PARAMETER_NAME;
+                } else {
+                    return ParameterRole.PARAMETER_DEFAULT;
+                }
+            } else if (idx == argDescsEnd) {
+                return ParameterRole.CATCH_ALL_PARAMETER_NAME;
+            } else if (idx == argDescsEnd + 1) {
+                return ParameterRole.AST_NODE_SUBTYPE;
+            } else {
+                throw new IndexOutOfBoundsException();
+            }
+        }
+    }
+    
+    @Override
+    public String toString() {
+        final UnboundTemplate unboundTemplate = getUnboundTemplate();
+        return "UnboundCallable("
+                + "name=" + getName()
+                + ", isFunction=" + isFunction()
+                + ", unboundTemplate"
+                + (unboundTemplate != null ? ".sourceName=" + unboundTemplate.getSourceName() : "=null")
+                + ")";
+    }
+
+}
diff --git a/src/main/java/freemarker/core/UnboundTemplate.java b/src/main/java/freemarker/core/UnboundTemplate.java
new file mode 100644
index 0000000..fac5be9
--- /dev/null
+++ b/src/main/java/freemarker/core/UnboundTemplate.java
@@ -0,0 +1,471 @@
+/*
+ * Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package freemarker.core;
+
+import java.io.BufferedReader;
+import java.io.FilterReader;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import freemarker.cache.TemplateLoader;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.Template.WrongEncodingException;
+import freemarker.template.Version;
+import freemarker.template._TemplateAPI;
+import freemarker.template.utility.NullArgumentException;
+
+/**
+ * The parsed representation of a template that's not yet bound to the {@link Template} properties that doesn't
+ * influence the result of the parsing. This information wasn't separated from {@link Template} in FreeMarker 2.3.x,
+ * and was factored out from it into this class in 2.4.0, to allow more efficient caching.
+ * 
+ * @since 2.4.0
+ */
+public final class UnboundTemplate {
+
+    public static final String DEFAULT_NAMESPACE_PREFIX = "D";
+    public static final String NO_NS_PREFIX = "N";
+
+    /**
+     * This is only non-null during parsing. It's used internally to make some information available through the
+     * Template API-s earlier than the parsing was finished.
+     */
+    private transient FMParser parser;
+
+    private final String sourceName;
+    private final Configuration cfg;
+    private final Version templateLanguageVersion;
+    
+    /** Attributes added via {@code <#ftl attributes=...>}. */
+    private LinkedHashMap<String, Object> customAttributes;
+    
+    private Map<String, UnboundCallable> unboundCallables = new HashMap<String, UnboundCallable>();
+    // Earlier it was a Vector, so I thought the safest is to keep it synchronized:
+    private final List<LibraryLoad> imports = Collections.synchronizedList(new ArrayList<LibraryLoad>());
+    private TemplateElement rootElement;
+    private String defaultNamespaceURI;
+    private int actualTagSyntax;
+    
+    private final ArrayList lines = new ArrayList();
+    
+    private Map<String, String> nodePrefixToNamespaceURIMapping = new HashMap<String, String>();
+    private Map<String, String> namespaceURIToPrefixMapping = new HashMap<String, String>();
+
+    private UnboundTemplate(String sourceName, Configuration cfg) {
+        this.sourceName = sourceName;
+        
+        NullArgumentException.check(cfg);
+        this.cfg = cfg;
+        
+        this.templateLanguageVersion = normalizeTemplateLanguageVersion(cfg.getIncompatibleImprovements());
+    }
+    
+    /**
+     * @param reader
+     *            Reads the template source code
+     * @param cfg
+     *            The FreeMarker configuration settings; some of them influences parsing, also the resulting
+     *            {@link UnboundTemplate} will be bound to this.
+     * @param assumedEncoding
+     *            This is the name of the charset that we are supposed to be using. This is only needed to check if the
+     *            encoding specified in the {@code #ftl} header (if any) matches this. If this is non-{@code null} and
+     *            they don't match, a {@link WrongEncodingException} will be thrown by the parser.
+     * @param sourceName
+     *            Shown in error messages as the template "file" location.
+     */
+    UnboundTemplate(Reader reader, String sourceName, Configuration cfg, String assumedEncoding)
+            throws IOException {
+        this(sourceName, cfg);
+
+        try {
+            if (!(reader instanceof BufferedReader)) {
+                reader = new BufferedReader(reader, 0x1000);
+            }
+            reader = new LineTableBuilder(reader);
+
+            try {
+                parser = new FMParser(this,
+                        reader, assumedEncoding,
+                        cfg.getStrictSyntaxMode(),
+                        cfg.getWhitespaceStripping(),
+                        cfg.getTagSyntax(),
+                        cfg.getIncompatibleImprovements().intValue());
+                this.rootElement = parser.Root();
+                this.actualTagSyntax = parser._getLastTagSyntax();
+            } catch (TokenMgrError exc) {
+                // TokenMgrError VS ParseException is not an interesting difference for the user, so we just convert it
+                // to ParseException
+                throw exc.toParseException(this);
+            } finally {
+                parser = null;
+            }
+        } catch (ParseException e) {
+            e.setTemplateName(getSourceName());
+            throw e;
+        } finally {
+            reader.close();
+        }
+
+        namespaceURIToPrefixMapping = Collections.unmodifiableMap(namespaceURIToPrefixMapping);
+        nodePrefixToNamespaceURIMapping = Collections.unmodifiableMap(nodePrefixToNamespaceURIMapping);
+    }
+    
+    private static Version normalizeTemplateLanguageVersion(Version incompatibleImprovements) {
+        _TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
+        int v = incompatibleImprovements.intValue();
+        if (v < _TemplateAPI.VERSION_INT_2_3_19) {
+            return Configuration.VERSION_2_3_0;
+        } else if (v > _TemplateAPI.VERSION_INT_2_3_21) {
+            return Configuration.VERSION_2_3_21;
+        } else { // if 2.3.19 or 2.3.20 or 2.3.21
+            return incompatibleImprovements;
+        }
+    }
+    
+    static UnboundTemplate createPlainTextTemplate(String sourceName, String content, Configuration config) {
+        UnboundTemplate unboundTemplate = new UnboundTemplate(sourceName, config);
+        unboundTemplate.rootElement = new TextBlock(content);
+        unboundTemplate.actualTagSyntax = config.getTagSyntax();
+        return unboundTemplate;
+    }
+
+    /**
+     * Returns a string representing the raw template text in canonical form.
+     */
+    public String toString() {
+        StringWriter sw = new StringWriter();
+        try {
+            dump(sw);
+        } catch (IOException ioe) {
+            throw new RuntimeException(ioe.getMessage());
+        }
+        return sw.toString();
+    }
+
+    /**
+     * The name that was actually used to load this template from the {@link TemplateLoader} (or from other custom
+     * storage mechanism). This is what should be shown in error messages as the error location.
+     * 
+     * @see Template#getSourceName()
+     */
+    public String getSourceName() {
+        return sourceName;
+    }
+
+    /**
+     * Return the template language (FTL) version used by this template. For now (2.3.21) this is the same as
+     * {@link Configuration#getIncompatibleImprovements()}, except that it's normalized to the lowest version where the
+     * template language was changed.
+     */
+    public Version getTemplateLanguageVersion() {
+        return templateLanguageVersion;
+    }
+
+    /**
+     * Returns the tag syntax the parser has chosen for this template. If the syntax could be determined, it's
+     * {@link Configuration#SQUARE_BRACKET_TAG_SYNTAX} or {@link Configuration#ANGLE_BRACKET_TAG_SYNTAX}. If the syntax
+     * couldn't be determined (like because there was no tags in the template, or it was a plain text template), this
+     * returns whatever the default is in the current configuration, so it's maybe
+     * {@link Configuration#AUTO_DETECT_TAG_SYNTAX}.
+     */
+    public int getActualTagSyntax() {
+        return actualTagSyntax;
+    }
+    
+    public Configuration getConfiguration() {
+        return cfg;
+    }
+
+    /**
+     * Dump the raw template in canonical form.
+     */
+    public void dump(PrintStream ps) {
+        ps.print(rootElement.getCanonicalForm());
+    }
+
+    /**
+     * Dump the raw template in canonical form.
+     */
+    public void dump(Writer out) throws IOException {
+        out.write(rootElement.getCanonicalForm());
+    }
+
+    /**
+     * Called by code internally to maintain a table of macros
+     */
+    void addUnboundCallable(UnboundCallable unboundCallable) {
+        unboundCallables.put(unboundCallable.getName(), unboundCallable);
+    }
+
+    /**
+     * Called by code internally to maintain a list of imports
+     */
+    void addImport(LibraryLoad libLoad) {
+        imports.add(libLoad);
+    }
+
+    /**
+     * Returns the template source at the location specified by the coordinates given, or {@code null} if unavailable.
+     * 
+     * @param beginColumn
+     *            the first column of the requested source, 1-based
+     * @param beginLine
+     *            the first line of the requested source, 1-based
+     * @param endColumn
+     *            the last column of the requested source, 1-based
+     * @param endLine
+     *            the last line of the requested source, 1-based
+     * @see freemarker.core.TemplateObject#getSource()
+     */
+    public String getSource(int beginColumn,
+            int beginLine,
+            int endColumn,
+            int endLine)
+    {
+        if (beginLine < 1 || endLine < 1) return null; // dynamically ?eval-ed expressions has no source available
+
+        // Our container is zero-based.
+        --beginLine;
+        --beginColumn;
+        --endColumn;
+        --endLine;
+        StringBuffer buf = new StringBuffer();
+        for (int i = beginLine; i <= endLine; i++) {
+            if (i < lines.size()) {
+                buf.append(lines.get(i));
+            }
+        }
+        int lastLineLength = lines.get(endLine).toString().length();
+        int trailingCharsToDelete = lastLineLength - endColumn - 1;
+        buf.delete(0, beginColumn);
+        buf.delete(buf.length() - trailingCharsToDelete, buf.length());
+        return buf.toString();
+    }
+
+    /**
+     * Used internally by the parser.
+     */
+    void setCustomAttribute(String key, Object value) {
+        LinkedHashMap<String, Object> attrs = customAttributes;
+        if (attrs == null) {
+            attrs = new LinkedHashMap<String, Object>();
+            customAttributes = attrs;
+        }
+        attrs.put(key, value);
+    }
+
+    /**
+     * Returns the {@link Map} of custom attributes that are normally coming from the {@code #ftl} header, or
+     * {@code null} if there was none. The returned {@code Map} must not be modified, and might changes during
+     * template parsing as new attributes are added by the parser (i.e., it's not a snapshot).
+     */
+    Map<String, ?> getCustomAttributes() {
+        return this.customAttributes;
+    }
+
+    /**
+     * @return the root TemplateElement object.
+     */
+    TemplateElement getRootTreeNode() {
+        return rootElement;
+    }
+
+    Map<String, UnboundCallable> getUnboundCallables() {
+        return unboundCallables;
+    }
+
+    List<LibraryLoad> getImports() {
+        return imports;
+    }
+
+    /**
+     * This is used internally.
+     */
+    void addPrefixToNamespaceURIMapping(String prefix, String nsURI) {
+        if (nsURI.length() == 0) {
+            throw new IllegalArgumentException("Cannot map empty string URI");
+        }
+        if (prefix.length() == 0) {
+            throw new IllegalArgumentException("Cannot map empty string prefix");
+        }
+        if (prefix.equals(NO_NS_PREFIX)) {
+            throw new IllegalArgumentException("The prefix: " + prefix
+                    + " cannot be registered, it's reserved for special internal use.");
+        }
+        if (nodePrefixToNamespaceURIMapping.containsKey(prefix)) {
+            throw new IllegalArgumentException("The prefix: '" + prefix + "' was repeated. This is illegal.");
+        }
+        if (namespaceURIToPrefixMapping.containsKey(nsURI)) {
+            throw new IllegalArgumentException("The namespace URI: " + nsURI
+                    + " cannot be mapped to 2 different prefixes.");
+        }
+        if (prefix.equals(DEFAULT_NAMESPACE_PREFIX)) {
+            this.defaultNamespaceURI = nsURI;
+        } else {
+            nodePrefixToNamespaceURIMapping.put(prefix, nsURI);
+            namespaceURIToPrefixMapping.put(nsURI, prefix);
+        }
+    }
+
+    public String getDefaultNamespaceURI() {
+        return this.defaultNamespaceURI;
+    }
+
+    /**
+     * @return The namespace URI mapped to this node value prefix, or {@code null}.
+     */
+    public String getNamespaceURIForPrefix(String prefix) {
+        if (prefix.equals("")) {
+            return defaultNamespaceURI == null ? "" : defaultNamespaceURI;
+        }
+        return nodePrefixToNamespaceURIMapping.get(prefix);
+    }
+
+    /**
+     * @return the prefix mapped to this nsURI in this template. (Or null if there is none.)
+     */
+    public String getPrefixForNamespaceURI(String nsURI) {
+        if (nsURI == null) {
+            return null;
+        }
+        if (nsURI.length() == 0) {
+            return defaultNamespaceURI == null ? "" : NO_NS_PREFIX;
+        }
+        if (nsURI.equals(defaultNamespaceURI)) {
+            return "";
+        }
+        return namespaceURIToPrefixMapping.get(nsURI);
+    }
+
+    /**
+     * @return the prefixed name, based on the ns_prefixes defined in this template's header for the local name and node
+     *         namespace passed in as parameters.
+     */
+    public String getPrefixedName(String localName, String nsURI) {
+        if (nsURI == null || nsURI.length() == 0) {
+            if (defaultNamespaceURI != null) {
+                return NO_NS_PREFIX + ":" + localName;
+            } else {
+                return localName;
+            }
+        }
+        if (nsURI.equals(defaultNamespaceURI)) {
+            return localName;
+        }
+        String prefix = getPrefixForNamespaceURI(nsURI);
+        if (prefix == null) {
+            return null;
+        }
+        return prefix + ":" + localName;
+    }
+
+    /**
+     * @return an array of the {@link TemplateElement}s containing the given column and line numbers.
+     */
+    List<TemplateElement> containingElements(int column, int line) {
+        final ArrayList<TemplateElement> elements = new ArrayList<TemplateElement>();
+        TemplateElement element = rootElement;
+        mainloop: while (element.contains(column, line)) {
+            elements.add(element);
+            for (Enumeration enumeration = element.children(); enumeration.hasMoreElements();) {
+                TemplateElement elem = (TemplateElement) enumeration.nextElement();
+                if (elem.contains(column, line)) {
+                    element = elem;
+                    continue mainloop;
+                }
+            }
+            break;
+        }
+        return elements.isEmpty() ? null : elements;
+    }
+
+    /**
+     * This is a helper class that builds up the line table info for us.
+     */
+    private class LineTableBuilder extends FilterReader {
+    
+        StringBuffer lineBuf = new StringBuffer();
+        int lastChar;
+    
+        /**
+         * @param r
+         *            the character stream to wrap
+         */
+        LineTableBuilder(Reader r) {
+            super(r);
+        }
+    
+        public int read() throws IOException {
+            int c = in.read();
+            handleChar(c);
+            return c;
+        }
+    
+        public int read(char cbuf[], int off, int len) throws IOException {
+            int numchars = in.read(cbuf, off, len);
+            for (int i = off; i < off + numchars; i++) {
+                char c = cbuf[i];
+                handleChar(c);
+            }
+            return numchars;
+        }
+    
+        public void close() throws IOException {
+            if (lineBuf.length() > 0) {
+                lines.add(lineBuf.toString());
+                lineBuf.setLength(0);
+            }
+            super.close();
+        }
+    
+        private void handleChar(int c) {
+            if (c == '\n' || c == '\r') {
+                if (lastChar == '\r' && c == '\n') { // CRLF under Windoze
+                    int lastIndex = lines.size() - 1;
+                    String lastLine = (String) lines.get(lastIndex);
+                    lines.set(lastIndex, lastLine + '\n');
+                } else {
+                    lineBuf.append((char) c);
+                    lines.add(lineBuf.toString());
+                    lineBuf.setLength(0);
+                }
+            }
+            else if (c == '\t') {
+                int numSpaces = 8 - (lineBuf.length() % 8);
+                for (int i = 0; i < numSpaces; i++) {
+                    lineBuf.append(' ');
+                }
+            }
+            else {
+                lineBuf.append((char) c);
+            }
+            lastChar = c;
+        }
+    }
+
+}
diff --git a/src/main/java/freemarker/core/UnifiedCall.java b/src/main/java/freemarker/core/UnifiedCall.java
index a9bdc52..4db7485 100644
--- a/src/main/java/freemarker/core/UnifiedCall.java
+++ b/src/main/java/freemarker/core/UnifiedCall.java
@@ -71,18 +71,18 @@
     }
 
     void accept(Environment env) throws TemplateException, IOException {
-        TemplateModel tm = nameExp.eval(env);
-        if (tm == Macro.DO_NOTHING_MACRO) return; // shortcut here.
-        if (tm instanceof Macro) {
-            Macro macro = (Macro) tm;
-            if (macro.isFunction() && !legacySyntax) {
+        final TemplateModel tm = nameExp.eval(env);
+        if (tm == UnboundCallable.NO_OP_MACRO) return; // shortcut here.
+        if (tm instanceof BoundCallable) {
+            final BoundCallable boundMacro = (BoundCallable) tm;
+            final Macro unboundMacro = boundMacro.getUnboundCallable();
+            if (unboundMacro.isFunction() && !legacySyntax) {
                 throw new _MiscTemplateException(env, new Object[] {
-                        "Routine ", new _DelayedJQuote(macro.getName()), " is a function, not a directive. "
+                        "Routine ", new _DelayedJQuote(unboundMacro.getName()), " is a function, not a directive. "
                         + "Functions can only be called from expressions, like in ${f()}, ${x + f()} or ",
                         "<@someDirective someParam=f() />", "." });
             }    
-            env.invoke(macro, namedArgs, positionalArgs, bodyParameterNames,
-                    nestedBlock);
+            env.invoke(boundMacro, namedArgs, positionalArgs, bodyParameterNames, nestedBlock);
         }
         else {
             boolean isDirectiveModel = tm instanceof TemplateDirectiveModel; 
@@ -337,7 +337,7 @@
     }
     
     public String getTemplateSourceName() {
-        return getTemplate().getSourceName();
+        return getUnboundTemplate().getSourceName();
     }
 
     boolean isNestedBlockRepeater() {
diff --git a/src/main/java/freemarker/core/_CoreAPI.java b/src/main/java/freemarker/core/_CoreAPI.java
index 66b165b..75cfc76 100644
--- a/src/main/java/freemarker/core/_CoreAPI.java
+++ b/src/main/java/freemarker/core/_CoreAPI.java
@@ -16,11 +16,16 @@
 
 package freemarker.core;
 
+import java.io.IOException;
+import java.io.Reader;
 import java.io.Writer;
 import java.util.Collections;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
 
+import freemarker.template.Configuration;
 import freemarker.template.Template;
 import freemarker.template.TemplateDirectiveBody;
 
@@ -109,6 +114,70 @@
         return cfgable.getSettingNames();
     }
 
+    public static Map<String, ?> getCustomAttributes(UnboundTemplate unboundTemplate) {
+        return unboundTemplate.getCustomAttributes();
+    }
+    
+    /**
+     * For emulating legacy {@link Template#addMacro(Macro)}.
+     */
+    public static void addMacro(UnboundTemplate unboundTemplate, Macro macro) {
+        // A bit of backward compatibility complication, since in 2.4 Macro was split to bound- and unbound callables:
+        final UnboundCallable unboundCallable; 
+        if (macro instanceof UnboundCallable) {
+            // It's coming from the AST:
+            unboundCallable = (UnboundCallable) macro;
+        } else if (macro instanceof BoundCallable) {
+            // It's coming from an FTL variable:
+            unboundCallable = ((BoundCallable) macro).getUnboundCallable(); 
+        } else {
+            // Impossible, Macro should have only two subclasses.
+            throw new BugException();
+        }
+        unboundTemplate.addUnboundCallable(unboundCallable);
+    }
+
+    public static void addImport(UnboundTemplate unboundTemplate, LibraryLoad libLoad) {
+        unboundTemplate.addImport(libLoad);
+    }
+    
+    public static UnboundTemplate newUnboundTemplate(Reader reader, String sourceName, Configuration cfg, String assumedEncoding) throws IOException {
+        return new UnboundTemplate(reader, sourceName, cfg, assumedEncoding);
+    }
+    
+    public static boolean isBoundCallable(Object obj) {
+        return obj instanceof BoundCallable;
+    }
+
+    public static UnboundTemplate createPlainTextTemplate(String sourceName, String content, Configuration config) {
+        return UnboundTemplate.createPlainTextTemplate(sourceName, content, config);
+    }
+    
+    /** Used for implementing the deprecated {@link Template} method with similar name. */
+    public static TemplateElement getRootTreeNode(UnboundTemplate unboundTemplate) {
+        return unboundTemplate.getRootTreeNode();
+    }
+    
+    /** Used for implementing the deprecated {@link Template} method with similar name. */
+    public static Map getMacros(UnboundTemplate unboundTemplate) {
+        return unboundTemplate.getUnboundCallables();
+    }
+
+    /** Used for implementing the deprecated {@link Template} method with similar name. */
+    public static List getImports(UnboundTemplate unboundTemplate) {
+        return unboundTemplate.getImports();
+    }
+    
+    /** Used for implementing the deprecated {@link Template} method with similar name. */
+    public static void addPrefixNSMapping(UnboundTemplate unboundTemplate, String prefix, String nsURI) {
+        unboundTemplate.addPrefixToNamespaceURIMapping(prefix, nsURI);
+    }
+    
+    /** Used for implementing the deprecated {@link Template} method with similar name. */
+    public static List<TemplateElement> containingElements(UnboundTemplate unboundTemplate, int column, int line) {
+        return unboundTemplate.containingElements(column, line);
+    }
+    
     /**
      * ATTENTION: This is used by https://github.com/kenshoo/freemarker-online. Don't break backward
      * compatibility without updating that project too! 
diff --git a/src/main/java/freemarker/core/_ErrorDescriptionBuilder.java b/src/main/java/freemarker/core/_ErrorDescriptionBuilder.java
index cf91654..6903b86 100644
--- a/src/main/java/freemarker/core/_ErrorDescriptionBuilder.java
+++ b/src/main/java/freemarker/core/_ErrorDescriptionBuilder.java
@@ -204,7 +204,9 @@
     }
 
     private void appendParts(StringBuffer sb, Object[] parts) {
-        Template template = this.template != null ? this.template : (blamed != null ? blamed.getTemplate() : null); 
+        UnboundTemplate unboundTemplate = this.template != null
+                ? this.template.getUnboundTemplate()
+                : (blamed != null ? blamed.getUnboundTemplate() : null); 
         for (int i = 0; i < parts.length; i++) {
             Object partObj = parts[i];
             if (partObj instanceof Object[]) {
@@ -216,7 +218,7 @@
                     partStr = "null";
                 }
                 
-                if (template != null) {
+                if (unboundTemplate != null) {
                     if (partStr.length() > 4
                             && partStr.charAt(0) == '<'
                             && (
@@ -224,7 +226,7 @@
                                     || (partStr.charAt(1) == '/') && (partStr.charAt(2) == '#' || partStr.charAt(2) == '@')
                                )
                             && partStr.charAt(partStr.length() - 1) == '>') {
-                        if (template.getActualTagSyntax() == Configuration.SQUARE_BRACKET_TAG_SYNTAX) {
+                        if (unboundTemplate.getActualTagSyntax() == Configuration.SQUARE_BRACKET_TAG_SYNTAX) {
                             sb.append('[');
                             sb.append(partStr.substring(1, partStr.length() - 1));
                             sb.append(']');
diff --git a/src/main/java/freemarker/template/Template.java b/src/main/java/freemarker/template/Template.java
index 9ae73af..8ae8bef 100644
--- a/src/main/java/freemarker/template/Template.java
+++ b/src/main/java/freemarker/template/Template.java
@@ -16,21 +16,14 @@
 
 package freemarker.template;
 
-import java.io.BufferedReader;
-import java.io.FilterReader;
 import java.io.IOException;
 import java.io.PrintStream;
 import java.io.Reader;
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.io.Writer;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Vector;
 
 import freemarker.cache.TemplateCache;
 import freemarker.cache.TemplateLoader;
@@ -42,9 +35,8 @@
 import freemarker.core.Macro;
 import freemarker.core.ParseException;
 import freemarker.core.TemplateElement;
-import freemarker.core.TextBlock;
-import freemarker.core.TokenMgrError;
-import freemarker.debug.impl.DebuggerService;
+import freemarker.core.UnboundTemplate;
+import freemarker.core._CoreAPI;
 
 /**
  * <p>Stores an already parsed template, ready to be processed (rendered) for unlimited times, possibly from
@@ -63,36 +55,27 @@
  * template object is already accessible from multiple threads.
  */
 public class Template extends Configurable {
-    public static final String DEFAULT_NAMESPACE_PREFIX = "D";
-    public static final String NO_NS_PREFIX = "N";
+    public static final String DEFAULT_NAMESPACE_PREFIX = UnboundTemplate.DEFAULT_NAMESPACE_PREFIX;
+    public static final String NO_NS_PREFIX = UnboundTemplate.NO_NS_PREFIX;
     
     /** This is only non-null during parsing. It's used internally to make some information available through the
      *  Template API-s earlier than the parsing was finished. */
     private transient FMParser parser;
 
-    private Map macros = new HashMap();
-    private List imports = new Vector();
-    private TemplateElement rootElement;
-    private String encoding, defaultNS;
-    private Object customLookupCondition;
-    private int actualTagSyntax;
+    private final UnboundTemplate unboundTemplate;
     private final String name;
-    private final String sourceName;
-    private final ArrayList lines = new ArrayList();
-    private Map prefixToNamespaceURILookup = new HashMap();
-    private Map namespaceURIToPrefixLookup = new HashMap();
-    private Version templateLanguageVersion;
+    private String encoding;
+    private Object customLookupCondition;
 
     /**
      * A prime constructor to which all other constructors should
      * delegate directly or indirectly.
      */
-    private Template(String name, String sourceName, Configuration cfg, boolean overloadSelector)
+    private Template(UnboundTemplate unboundTemplate, String name, Configuration cfg)
     {
         super(toNonNull(cfg));
+        this.unboundTemplate = unboundTemplate; 
         this.name = name;
-        this.sourceName = sourceName;
-        this.templateLanguageVersion = normalizeTemplateLanguageVersion(toNonNull(cfg).getIncompatibleImprovements());
     }
 
     private static Configuration toNonNull(Configuration cfg) {
@@ -180,43 +163,9 @@
      */
     public Template(
             String name, String sourceName, Reader reader, Configuration cfg, String encoding) throws IOException {
-        this(name, sourceName, cfg, true);
-        
+        this(_CoreAPI.newUnboundTemplate(
+                reader, sourceName != null ? sourceName : name, toNonNull(cfg), encoding), name, cfg);
         this.encoding = encoding;
-        try {
-            if (!(reader instanceof BufferedReader)) {
-                reader = new BufferedReader(reader, 0x1000);
-            }
-            reader = new LineTableBuilder(reader);
-            
-            try {
-                parser = new FMParser(this, reader,
-                        getConfiguration().getStrictSyntaxMode(),
-                        getConfiguration().getWhitespaceStripping(),
-                        getConfiguration().getTagSyntax(),
-                        getConfiguration().getIncompatibleImprovements().intValue());
-                this.rootElement = parser.Root();
-                this.actualTagSyntax = parser._getLastTagSyntax();
-            }
-            catch (TokenMgrError exc) {
-                // TokenMgrError VS ParseException is not an interesting difference for the user, so we just convert it
-                // to ParseException
-                throw exc.toParseException(this);
-            }
-            finally {
-                parser = null;
-            }
-        }
-        catch(ParseException e) {
-            e.setTemplateName(getSourceName());
-            throw e;
-        }
-        finally {
-            reader.close();
-        }
-        DebuggerService.registerTemplate(this);
-        namespaceURIToPrefixLookup = Collections.unmodifiableMap(namespaceURIToPrefixLookup);
-        prefixToNamespaceURILookup = Collections.unmodifiableMap(prefixToNamespaceURILookup);
     }
 
     /**
@@ -232,18 +181,6 @@
     }
 
     /**
-     * Only meant to be used internally.
-     * 
-     * @deprecated Has problems setting actualTagSyntax and templateLanguageVersion; will be removed in 2.4.
-     */
-    // [2.4] remove this
-    Template(String name, TemplateElement root, Configuration cfg) {
-        this(name, null, cfg, true);
-        this.rootElement = root;
-        DebuggerService.registerTemplate(this);
-    }
-    
-    /**
      * Same as {@link #getPlainTextTemplate(String, String, String, Configuration)} with {@code null} {@code sourceName}
      * argument.
      */
@@ -266,23 +203,9 @@
      * @since 2.3.22
      */
     static public Template getPlainTextTemplate(String name, String sourceName, String content, Configuration config) {
-        Template template = new Template(name, sourceName, config, true);
-        template.rootElement = new TextBlock(content);
-        template.actualTagSyntax = config.getTagSyntax();
-        DebuggerService.registerTemplate(template);
-        return template;
-    }
-
-    private static Version normalizeTemplateLanguageVersion(Version incompatibleImprovements) {
-        _TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
-        int v = incompatibleImprovements.intValue();
-        if (v < _TemplateAPI.VERSION_INT_2_3_19) {
-            return Configuration.VERSION_2_3_0;
-        } else if (v > _TemplateAPI.VERSION_INT_2_3_21) {
-            return Configuration.VERSION_2_3_21;
-        } else { // if 2.3.19 or 2.3.20 or 2.3.21
-            return incompatibleImprovements;
-        }
+        return new Template(_CoreAPI.createPlainTextTemplate(
+                sourceName != null ? sourceName : name,
+                content, config), name, config);
     }
 
     /**
@@ -444,6 +367,14 @@
         return sw.toString();
     }
 
+    /**
+     * Returns the {@link UnboundTemplate} that this {@link Template} is based on.
+     * 
+     * @since 2.4.0
+     */
+    public UnboundTemplate getUnboundTemplate() {
+        return unboundTemplate;
+    }
 
     /**
      * The usually path-like (or URL-like) identifier of the template, or possibly {@code null} for non-stored
@@ -490,7 +421,7 @@
      * @since 2.3.22
      */
     public String getSourceName() {
-        return sourceName != null ? sourceName : getName();
+        return unboundTemplate.getSourceName();
     }
 
     /**
@@ -502,11 +433,13 @@
     
     /**
      * Return the template language (FTL) version used by this template.
-     * For now (2.3.21) this is the same as {@link Configuration#getIncompatibleImprovements()}, except
+     * For now (2.4.0) this is the same as {@link Configuration#getIncompatibleImprovements()}, except
      * that it's normalized to the lowest version where the template language was changed.
+     * 
+     * @since 2.4.0
      */
-    Version getTemplateLanguageVersion() {
-        return templateLanguageVersion;
+    public Version getTemplateLanguageVersion() {
+        return unboundTemplate.getTemplateLanguageVersion();
     }
 
     /**
@@ -556,39 +489,35 @@
      * @since 2.3.20
      */
     public int getActualTagSyntax() {
-        return actualTagSyntax;
+        return unboundTemplate.getActualTagSyntax();
     }
 
     /**
      * Dump the raw template in canonical form.
      */
     public void dump(PrintStream ps) {
-        ps.print(rootElement.getCanonicalForm());
+        unboundTemplate.dump(ps);
     }
 
     /**
      * Dump the raw template in canonical form.
      */
     public void dump(Writer out) throws IOException {
-        out.write(rootElement.getCanonicalForm());
+        unboundTemplate.dump(out);
     }
 
     /**
-     * Called by code internally to maintain a table of macros
-     * 
      * @deprecated Should only be used internally, and might will be removed later.
      */
     public void addMacro(Macro macro) {
-        macros.put(macro.getName(), macro);
+        _CoreAPI.addMacro(unboundTemplate, macro);
     }
 
     /**
-     * Called by code internally to maintain a list of imports
-     * 
      * @deprecated Should only be used internally, and might will be removed later.
      */
-    public void addImport(LibraryLoad ll) {
-        imports.add(ll);
+    public void addImport(LibraryLoad libLoad) {
+        _CoreAPI.addImport(unboundTemplate, libLoad);
     }
 
     /**
@@ -599,114 +528,29 @@
      * @param endLine the last line of the requested source, 1-based
      * @see freemarker.core.TemplateObject#getSource()
      */
-    public String getSource(int beginColumn,
-                            int beginLine,
-                            int endColumn,
-                            int endLine)
-    {
-        if (beginLine < 1 || endLine < 1) return null;  // dynamically ?eval-ed expressions has no source available
-        
-        // Our container is zero-based.
-        --beginLine;
-        --beginColumn;
-        --endColumn;
-        --endLine;
-        StringBuffer buf = new StringBuffer();
-        for (int i = beginLine ; i<=endLine; i++) {
-            if (i < lines.size()) {
-                buf.append(lines.get(i));
-            }
-        }
-        int lastLineLength = lines.get(endLine).toString().length();
-        int trailingCharsToDelete = lastLineLength - endColumn -1;
-        buf.delete(0, beginColumn);
-        buf.delete(buf.length() - trailingCharsToDelete, buf.length());
-        return buf.toString();
-    }
-
-    /**
-     * This is a helper class that builds up the line table
-     * info for us.
-     */
-    private class LineTableBuilder extends FilterReader {
-
-        StringBuffer lineBuf = new StringBuffer();
-        int lastChar;
-
-        /**
-         * @param r the character stream to wrap
-         */
-        LineTableBuilder(Reader r) {
-            super(r);
-        }
-
-        public int read() throws IOException {
-            int c = in.read();
-            handleChar(c);
-            return c;
-        }
-
-        public int read(char cbuf[], int off, int len) throws IOException {
-            int numchars = in.read(cbuf, off, len);
-            for (int i=off; i < off+numchars; i++) {
-                char c = cbuf[i];
-                handleChar(c);
-            }
-            return numchars;
-        }
-
-        public void close() throws IOException {
-            if (lineBuf.length() >0) {
-                lines.add(lineBuf.toString());
-                lineBuf.setLength(0);
-            }
-            super.close();
-        }
-
-        private void handleChar(int c) {
-            if (c == '\n' || c == '\r') {
-                if (lastChar == '\r' && c == '\n') { // CRLF under Windoze
-                    int lastIndex = lines.size() -1;
-                    String lastLine = (String) lines.get(lastIndex);
-                    lines.set(lastIndex, lastLine + '\n');
-                } else {
-                    lineBuf.append((char) c);
-                    lines.add(lineBuf.toString());
-                    lineBuf.setLength(0);
-                }
-            }
-            else if (c == '\t') {
-                int numSpaces = 8 - (lineBuf.length() %8);
-                for (int i=0; i<numSpaces; i++) {
-                    lineBuf.append(' ');
-                }
-            }
-            else {
-                lineBuf.append((char) c);
-            }
-            lastChar = c;
-        }
+    public String getSource(int beginColumn, int beginLine, int endColumn, int endLine) {
+        return unboundTemplate.getSource(beginColumn, beginLine, endColumn, endLine);
     }
 
     /**
      * @deprecated Should only be used internally, and might will be removed later.
      */
     public TemplateElement getRootTreeNode() {
-        return rootElement;
+        return _CoreAPI.getRootTreeNode(unboundTemplate);
     }
     
     /**
      * @deprecated Should only be used internally, and might will be removed later.
      */
     public Map getMacros() {
-        return macros;
+        return _CoreAPI.getMacros(unboundTemplate);
     }
 
     /**
      * @deprecated Should only be used internally, and might will be removed later.
      */
     public List getImports() {
-        return imports;
+        return _CoreAPI.getImports(unboundTemplate);
     }
 
     /**
@@ -715,57 +559,25 @@
      * @deprecated Should only be used internally, and might will be removed later.
      */
     public void addPrefixNSMapping(String prefix, String nsURI) {
-        if (nsURI.length() == 0) {
-            throw new IllegalArgumentException("Cannot map empty string URI");
-        }
-        if (prefix.length() == 0) {
-            throw new IllegalArgumentException("Cannot map empty string prefix");
-        }
-        if (prefix.equals(NO_NS_PREFIX)) {
-            throw new IllegalArgumentException("The prefix: " + prefix + " cannot be registered, it's reserved for special internal use.");
-        }
-        if (prefixToNamespaceURILookup.containsKey(prefix)) {
-            throw new IllegalArgumentException("The prefix: '" + prefix + "' was repeated. This is illegal.");
-        }
-        if (namespaceURIToPrefixLookup.containsKey(nsURI)) {
-            throw new IllegalArgumentException("The namespace URI: " + nsURI + " cannot be mapped to 2 different prefixes.");
-        }
-        if (prefix.equals(DEFAULT_NAMESPACE_PREFIX)) {
-            this.defaultNS = nsURI;
-        } else {
-            prefixToNamespaceURILookup.put(prefix, nsURI);
-            namespaceURIToPrefixLookup.put(nsURI, prefix);
-        }
+        _CoreAPI.addPrefixNSMapping(unboundTemplate, prefix, nsURI);
     }
     
     public String getDefaultNS() {
-        return this.defaultNS;
+        return unboundTemplate.getDefaultNamespaceURI();
     }
     
     /**
      * @return the NamespaceUri mapped to this prefix in this template. (Or null if there is none.)
      */
     public String getNamespaceForPrefix(String prefix) {
-        if (prefix.equals("")) {
-            return defaultNS == null ? "" : defaultNS;
-        }
-        return (String) prefixToNamespaceURILookup.get(prefix);
+        return unboundTemplate.getNamespaceURIForPrefix(prefix);
     }
     
     /**
      * @return the prefix mapped to this nsURI in this template. (Or null if there is none.)
      */
     public String getPrefixForNamespace(String nsURI) {
-        if (nsURI == null) {
-            return null;
-        }
-        if (nsURI.length() == 0) {
-            return defaultNS == null ? "" : NO_NS_PREFIX;
-        }
-        if (nsURI.equals(defaultNS)) {
-            return "";
-        }
-        return (String) namespaceURIToPrefixLookup.get(nsURI);
+        return unboundTemplate.getPrefixForNamespaceURI(nsURI);
     }
     
     /**
@@ -774,46 +586,26 @@
      * passed in as parameters.
      */
     public String getPrefixedName(String localName, String nsURI) {
-        if (nsURI == null || nsURI.length() == 0) {
-            if (defaultNS != null) {
-                return NO_NS_PREFIX + ":" + localName;
-            } else {
-                return localName;
-            }
-        } 
-        if (nsURI.equals(defaultNS)) {
-            return localName;
-        } 
-        String prefix = getPrefixForNamespace(nsURI);
-        if (prefix == null) {
-            return null;
-        }
-        return prefix + ":" + localName;
+        return unboundTemplate.getPrefixedName(localName, nsURI);
     }
     
     /**
      * @return an array of the {@link TemplateElement}s containing the given column and line numbers.
      * @deprecated Should only be used internally, and might will be removed later.
+     * 
+     * @deprecated The objects building up templates aren't part of the published API, and are subject to change.
      */
     public List containingElements(int column, int line) {
-        final ArrayList elements = new ArrayList();
-        TemplateElement element = rootElement;
-        mainloop: while (element.contains(column, line)) {
-            elements.add(element);
-            for (Enumeration enumeration = element.children(); enumeration.hasMoreElements();) {
-                TemplateElement elem = (TemplateElement) enumeration.nextElement();
-                if (elem.contains(column, line)) {
-                    element = elem;
-                    continue mainloop;
-                }
-            }
-            break;
-        }
-        return elements.isEmpty() ? null : elements;
+        return _CoreAPI.containingElements(unboundTemplate, column, line);
+    }
+
+    @Override
+    protected Map<String, ?> getInitialCustomAttributes() {
+        return _CoreAPI.getCustomAttributes(unboundTemplate);
     }
 
     /**
-     * Thrown by the {@link Template} constructors that specify a non-{@code null} encoding whoch doesn't match the
+     * Thrown by the {@link Template} constructors that specify a non-{@code null} encoding which doesn't match the
      * encoding specified in the {@code #ftl} header of the template.
      */
     static public class WrongEncodingException extends ParseException {
diff --git a/src/main/java/freemarker/template/TemplateException.java b/src/main/java/freemarker/template/TemplateException.java
index 5c735d3..1016a59 100644
--- a/src/main/java/freemarker/template/TemplateException.java
+++ b/src/main/java/freemarker/template/TemplateException.java
@@ -30,6 +30,7 @@
 import freemarker.core.ParseException;
 import freemarker.core.TemplateElement;
 import freemarker.core.TemplateObject;
+import freemarker.core.UnboundTemplate;
 import freemarker.core._CoreAPI;
 import freemarker.core._ErrorDescriptionBuilder;
 import freemarker.template.utility.CollectionUtils;
@@ -40,7 +41,6 @@
  */
 public class TemplateException extends Exception {
 
-    private static final int FTL_STACK_TOP_FEW_MAX_LINES = 6;
     private static final String FTL_INSTRUCTION_STACK_TRACE_TITLE
             = "FTL stack trace (\"~\" means nesting-related):";
 
@@ -197,11 +197,11 @@
                         : (
                                 ftlInstructionStackSnapshot != null && ftlInstructionStackSnapshot.length != 0
                                 ? ftlInstructionStackSnapshot[0] : null);
-                // Line number blow 0 means no info, negative means position in ?eval-ed value that we won't use here.
+                // Line number below 0 means no info, negative means position in ?eval-ed value that we won't use here.
                 if (templateObject != null && templateObject.getBeginLine() > 0) {
-                    final Template template = templateObject.getTemplate();
-                    templateName = template != null ? template.getName() : null;
-                    templateSourceName = template != null ? template.getSourceName() : null;
+                    final UnboundTemplate unboundTemplate = templateObject.getUnboundTemplate();
+                    templateName = getTemplateNameOrNull(unboundTemplate);
+                    templateSourceName = unboundTemplate != null ? unboundTemplate.getSourceName() : null;
                     lineNumber = new Integer(templateObject.getBeginLine());
                     columnNumber = new Integer(templateObject.getBeginColumn());
                     endLineNumber = new Integer(templateObject.getEndLine());
@@ -212,6 +212,19 @@
             }
         }
     }
+
+    private String getTemplateNameOrNull(final UnboundTemplate unboundTemplate) {
+        if (unboundTemplate == null) {
+            return null;
+        }
+        
+        Template template = env != null ? env.getCurrentTemplate() : null;
+        if (template == null) {
+            return null;
+        }
+        
+        return template.getUnboundTemplate() == unboundTemplate ? template.getName() : null;
+    }
     
     /**
      * @deprecated Java 1.4 has introduced {@link #getCause()} - use that instead, especially as this can't return
diff --git a/src/main/java/freemarker/template/_TemplateAPI.java b/src/main/java/freemarker/template/_TemplateAPI.java
index 655d455..3e99ba9 100644
--- a/src/main/java/freemarker/template/_TemplateAPI.java
+++ b/src/main/java/freemarker/template/_TemplateAPI.java
@@ -59,13 +59,9 @@
     }
     
     public static int getTemplateLanguageVersionAsInt(TemplateObject to) {
-        return getTemplateLanguageVersionAsInt(to.getTemplate());
+        return to.getUnboundTemplate().getTemplateLanguageVersion().intValue();
     }
 
-    public static int getTemplateLanguageVersionAsInt(Template t) {
-        return t.getTemplateLanguageVersion().intValue();
-    }
-    
     /** For unit testing only */
     public static void DefaultObjectWrapperFactory_clearInstanceCache() {
         DefaultObjectWrapperBuilder.clearInstanceCache();
diff --git a/src/main/java/freemarker/template/utility/ClassUtil.java b/src/main/java/freemarker/template/utility/ClassUtil.java
index fb92843..53dd9d2 100644
--- a/src/main/java/freemarker/template/utility/ClassUtil.java
+++ b/src/main/java/freemarker/template/utility/ClassUtil.java
@@ -21,6 +21,7 @@
 
 import freemarker.core.Environment;
 import freemarker.core.Macro;
+import freemarker.core._CoreAPI;
 import freemarker.ext.beans.BeanModel;
 import freemarker.ext.beans.BooleanModel;
 import freemarker.ext.beans.CollectionModel;
@@ -286,7 +287,7 @@
                 appendTemplateModelTypeName(sb, typeNamesAppended, primaryInterface);
             }
     
-            if (tm instanceof Macro) {
+            if (_CoreAPI.isBoundCallable(tm)) {
                 appendTypeName(sb, typeNamesAppended, ((Macro) tm).isFunction() ? "function" : "macro");
             }
             
diff --git a/src/main/java/freemarker/template/utility/CollectionUtils.java b/src/main/java/freemarker/template/utility/CollectionUtils.java
index 9413c03..330fa5e 100644
--- a/src/main/java/freemarker/template/utility/CollectionUtils.java
+++ b/src/main/java/freemarker/template/utility/CollectionUtils.java
@@ -23,6 +23,11 @@
 
     public static final Object[] EMPTY_OBJECT_ARRAY = new Object[] { };
 
+    /**
+     * @since 2.3.22
+     */
+    public static final String[] EMPTY_STRING_ARRAY = new String[] { };
+    
     public static final Class[] EMPTY_CLASS_ARRAY = new Class[] { };
 
     /**
diff --git a/src/main/java/freemarker/template/utility/StringUtil.java b/src/main/java/freemarker/template/utility/StringUtil.java
index ecf2716..ede1118 100644
--- a/src/main/java/freemarker/template/utility/StringUtil.java
+++ b/src/main/java/freemarker/template/utility/StringUtil.java
@@ -528,7 +528,7 @@
         do {
             buf.append(s.substring(bidx, idx));
             if (idx >= lidx) {
-                throw new ParseException("The last character of string literal is backslash", 0,0);
+                throw new ParseException("The last character of string literal is backslash", 0, 0);
             }
             char c = s.charAt(idx + 1);
             switch (c) {
diff --git a/src/main/javacc/FTL.jj b/src/main/javacc/FTL.jj
index ff778fe..4de27e1 100644
--- a/src/main/javacc/FTL.jj
+++ b/src/main/javacc/FTL.jj
@@ -38,11 +38,13 @@
 public class FMParser {
 
     // Necessary for adding macros and setting location info.
-    private Template template;
+    private UnboundTemplate unboundTemplate;
+    private String assumedEncoding;
+    private boolean stripWhitespace, stripText;
 
     // variables that keep track of whether we are in a loop or a switch.
     private int loopNesting, switchNesting;
-    private boolean inMacro, inFunction, stripWhitespace, stripText;
+    private boolean inMacro, inFunction;
     private LinkedList escapes = new LinkedList();
     private int mixedContentNesting; // for stripText
     private int incompatibleImprovements;
@@ -59,32 +61,22 @@
         return parser;
     }
 
-    /**
-     * Constructs a new parser object.
-     * 
-     * @param template
-     *            The template associated with this parser.
-     * @param reader
-     *            The character stream to use as input
-     * @param strictEscapeSyntax
-     *            Whether FreeMarker directives must start with a #
-     */
-    public FMParser(Template template, Reader reader, boolean strictEscapeSyntax, boolean stripWhitespace) {
-        this(reader);
-        setTemplate(template);
-        token_source.setParser(this);
-        token_source.strictEscapeSyntax = strictEscapeSyntax;
-        this.stripWhitespace = stripWhitespace;
-    }
-
-    public FMParser(Template template, Reader reader, boolean strictEscapeSyntax, boolean stripWhitespace, int tagSyntax) {
-        this(template, reader, strictEscapeSyntax, stripWhitespace, tagSyntax,
-                Configuration.PARSED_DEFAULT_INCOMPATIBLE_ENHANCEMENTS);
-    }
-
-    public FMParser(Template template, Reader reader, boolean strictEscapeSyntax, boolean stripWhitespace,
+    FMParser(
+            UnboundTemplate unboundTemplate,
+            Reader reader, String assumedEncoding,
+            boolean strictEscapeSyntax, boolean stripWhitespace,
             int tagSyntax, int incompatibleImprovements) {
-        this(template, reader, strictEscapeSyntax, stripWhitespace);
+        this(reader);
+        this.assumedEncoding = assumedEncoding;
+        
+        setTemplate(unboundTemplate);
+        
+        token_source.setParser(this);
+        
+        token_source.strictEscapeSyntax = strictEscapeSyntax;
+        
+        this.stripWhitespace = stripWhitespace;
+        
         switch (tagSyntax) {
         case Configuration.AUTO_DETECT_TAG_SYNTAX:
             token_source.autodetectTagSyntax = true;
@@ -98,22 +90,19 @@
         default:
             throw new IllegalArgumentException("Illegal argument for tagSyntax");
         }
+        
         token_source.incompatibleImprovements = incompatibleImprovements;
         this.incompatibleImprovements = incompatibleImprovements;
     }
 
-    public FMParser(String template) {
-        this(null, new StringReader(template), true, true);
+    void setTemplate(UnboundTemplate unboundTemplate)
+    {
+        this.unboundTemplate = unboundTemplate;
     }
 
-    void setTemplate(Template template)
+    UnboundTemplate getTemplate()
     {
-        this.template = template;
-    }
-
-    Template getTemplate()
-    {
-        return template;
+        return unboundTemplate;
     }
 
     /**
@@ -314,7 +303,7 @@
         this.parser = parser;
     }
 
-    Template getTemplate() {
+    UnboundTemplate getTemplate() {
         return parser != null ? parser.getTemplate() : null;
     }
 
@@ -1127,7 +1116,7 @@
     end = <CLOSE_PAREN>
     {
         result = new ParentheticalExpression(exp);
-        result.setLocation(template, start, end);
+        result.setLocation(unboundTemplate, start, end);
         return result;
     }
 }
@@ -1171,7 +1160,7 @@
         for (int i = 0; i < nots.size(); i++) {
             result = new NotExpression(exp);
             Token tok = (Token) nots.get(nots.size() -i -1);
-            result.setLocation(template, tok, exp);
+            result.setLocation(unboundTemplate, tok, exp);
             exp = result;
         }
         return result;
@@ -1193,7 +1182,7 @@
     exp = PrimaryExpression()
     {
         result = new UnaryPlusMinusExpression(exp, isMinus);  
-        result.setLocation(template, t, exp);
+        result.setLocation(unboundTemplate, t, exp);
         return result;
     }
 }
@@ -1225,7 +1214,7 @@
                 numberLiteralOnly(rhs);
                 result = new ArithmeticExpression(lhs, rhs, ArithmeticExpression.TYPE_SUBSTRACTION);
             }
-            result.setLocation(template, lhs, rhs);
+            result.setLocation(unboundTemplate, lhs, rhs);
             lhs = result;
         }
     )*
@@ -1261,7 +1250,7 @@
             numberLiteralOnly(lhs);
             numberLiteralOnly(rhs);
             result = new ArithmeticExpression(lhs, rhs, operation);
-            result.setLocation(template, lhs, rhs);
+            result.setLocation(unboundTemplate, lhs, rhs);
             lhs = result;
         }
     )*
@@ -1294,7 +1283,7 @@
 	        notListLiteral(lhs, "scalar");
 	        notListLiteral(rhs, "scalar");
 	        result = new ComparisonExpression(lhs, rhs, t.image);
-	        result.setLocation(template, lhs, rhs);
+	        result.setLocation(unboundTemplate, lhs, rhs);
         }
     ]
     {
@@ -1333,7 +1322,7 @@
             notStringLiteral(lhs, "number");
             notStringLiteral(rhs, "number");
             result = new ComparisonExpression(lhs, rhs, t.image);
-            result.setLocation(template, lhs, rhs);
+            result.setLocation(unboundTemplate, lhs, rhs);
         }
     ]
     {
@@ -1380,9 +1369,9 @@
            
             Range range = new Range(lhs, rhs, endType);
             if (rhs != null) {
-                range.setLocation(template, lhs, rhs);
+                range.setLocation(unboundTemplate, lhs, rhs);
             } else {
-                range.setLocation(template, lhs, dotDot);
+                range.setLocation(unboundTemplate, lhs, dotDot);
             }
             result = range;
         }
@@ -1409,7 +1398,7 @@
             booleanLiteralOnly(lhs);
             booleanLiteralOnly(rhs);
             result = new AndExpression(lhs, rhs);
-            result.setLocation(template, lhs, rhs);
+            result.setLocation(unboundTemplate, lhs, rhs);
             lhs = result;
         }
     )*
@@ -1432,7 +1421,7 @@
             booleanLiteralOnly(lhs);
             booleanLiteralOnly(rhs);
             result = new OrExpression(lhs, rhs);
-            result.setLocation(template, lhs, rhs);
+            result.setLocation(unboundTemplate, lhs, rhs);
             lhs = result;
         }
     )*
@@ -1452,7 +1441,7 @@
     end = <CLOSE_BRACKET>
     {
         ListLiteral result = new ListLiteral(values);
-        result.setLocation(template, begin, end);
+        result.setLocation(unboundTemplate, begin, end);
         return result;
     }
 }
@@ -1469,9 +1458,9 @@
     )
     {
         String s = t.image;
-        Expression result = new NumberLiteral(template.getArithmeticEngine().toNumber(s));
+        Expression result = new NumberLiteral(unboundTemplate.getConfiguration().getArithmeticEngine().toNumber(s));
         Token startToken = (op != null) ? op : t;
-        result.setLocation(template, startToken, t);
+        result.setLocation(unboundTemplate, startToken, t);
         return result;
     }
 }
@@ -1484,7 +1473,7 @@
     t = <ID>
     {
         Identifier id = new Identifier(t.image);
-        id.setLocation(template, t, t);
+        id.setLocation(unboundTemplate, t, t);
         return id;
     }
 }
@@ -1522,7 +1511,7 @@
             pe.endColumnNumber = name.endColumn;
             throw pe;
         }
-        result.setLocation(template, dot, name);
+        result.setLocation(unboundTemplate, dot, name);
         return result;
     }
 }
@@ -1575,9 +1564,9 @@
     {
         DefaultToExpression result = new DefaultToExpression(exp, rhs);
         if (rhs == null) {
-            result.setLocation(template, exp, t);
+            result.setLocation(unboundTemplate, exp, t);
         } else {
-            result.setLocation(template, exp, rhs);
+            result.setLocation(unboundTemplate, exp, rhs);
         }
         return result;
     }
@@ -1591,7 +1580,7 @@
     t = <EXISTS>
     {
         ExistsExpression result = new ExistsExpression(exp);
-        result.setLocation(template, exp, t);
+        result.setLocation(unboundTemplate, exp, t);
         return result;
     }
 }
@@ -1614,7 +1603,7 @@
             pe.endColumnNumber = t.endColumn;
             throw pe;
         }
-        result.setLocation(template, exp, t);
+        result.setLocation(unboundTemplate, exp, t);
         return result;
     }
 }
@@ -1653,7 +1642,7 @@
             )
             {
                 if (!Character.isLetter(t.image.charAt(0))) {
-                    throw new ParseException(t.image + " is not a valid identifier.", template, t);
+                    throw new ParseException(t.image + " is not a valid identifier.", unboundTemplate, t);
                 }
             }
         )
@@ -1662,7 +1651,7 @@
             notStringLiteral(exp, "hash");
             notBooleanLiteral(exp, "hash");
             Dot dot = new Dot(exp, t.image);
-            dot.setLocation(template, exp, t);
+            dot.setLocation(unboundTemplate, exp, t);
             return dot;
         }
 }
@@ -1684,7 +1673,7 @@
         notBooleanLiteral(exp, "list or hash");
         notNumberLiteral(exp, "list or hash");
         DynamicKeyName dkn = new DynamicKeyName(exp, arg);
-        dkn.setLocation(template, exp, t);
+        dkn.setLocation(unboundTemplate, exp, t);
         return dkn;
     }
 }
@@ -1704,7 +1693,7 @@
         {
             args.trimToSize();
             MethodCall result = new MethodCall(exp, args);
-            result.setLocation(template, exp, end);
+            result.setLocation(unboundTemplate, exp, end);
             return result;
         }
 }
@@ -1736,7 +1725,7 @@
             throw pe;
         }
         StringLiteral result = new StringLiteral(s);
-        result.setLocation(template, t, t);
+        result.setLocation(unboundTemplate, t, t);
         if (interpolate && !raw) {
             if (t.image.indexOf("${") >= 0 || t.image.indexOf("#{") >= 0) result.checkInterpolation();
         }
@@ -1756,7 +1745,7 @@
         t = <TRUE> { result = new BooleanLiteral(true); }
     )
     {
-        result.setLocation(template, t, t);
+        result.setLocation(unboundTemplate, t, t);
         return result;
     }
 }
@@ -1795,7 +1784,7 @@
     end = <CLOSING_CURLY_BRACKET>
     {
         HashLiteral result = new HashLiteral(keys, values);
-        result.setLocation(template, begin, end);
+        result.setLocation(unboundTemplate, begin, end);
         return result;
     }
 }
@@ -1819,7 +1808,7 @@
     end = <CLOSING_CURLY_BRACKET>
     {
         DollarVariable result = new DollarVariable(exp, escapedExpression(exp));
-        result.setLocation(template, begin, end);
+        result.setLocation(unboundTemplate, begin, end);
         return result;
     }
 }
@@ -1851,15 +1840,15 @@
 	                if (type != '-') {
 	                    switch (type) {
 	                    case 'm':
-	                        if (minFrac != -1) throw new ParseException("Invalid formatting string", template, fmt);
+	                        if (minFrac != -1) throw new ParseException("Invalid formatting string", unboundTemplate, fmt);
 	                        minFrac = Integer.parseInt(token);
 	                        break;
 	                    case 'M':
-	                        if (maxFrac != -1) throw new ParseException("Invalid formatting string", template, fmt);
+	                        if (maxFrac != -1) throw new ParseException("Invalid formatting string", unboundTemplate, fmt);
 	                        maxFrac = Integer.parseInt(token);
 	                        break;
 	                    default:
-	                        throw new ParseException("Invalid formatting string", template, fmt);
+	                        throw new ParseException("Invalid formatting string", unboundTemplate, fmt);
 	                    }
 	                    type = '-';
 	                } else if (token.equals("m")) {
@@ -1870,16 +1859,16 @@
 	                    throw new ParseException();
 	                }
                 } catch (ParseException e) {
-                	throw new ParseException("Invalid format specifier " + fmt.image, template, fmt);
+                	throw new ParseException("Invalid format specifier " + fmt.image, unboundTemplate, fmt);
                 } catch (NumberFormatException e) {
-                	throw new ParseException("Invalid number in the format specifier " + fmt.image, template, fmt);
+                	throw new ParseException("Invalid number in the format specifier " + fmt.image, unboundTemplate, fmt);
                 }
             }
 
             if (maxFrac == -1) {
 	            if (minFrac == -1) {
 	                throw new ParseException(
-	                		"Invalid format specification, at least one of m and M must be specified!", template, fmt);
+	                		"Invalid format specification, at least one of m and M must be specified!", unboundTemplate, fmt);
 	            }
             	maxFrac = minFrac;
             } else if (minFrac == -1) {
@@ -1887,16 +1876,16 @@
             }
             if (minFrac > maxFrac) {
             	throw new ParseException(
-            			"Invalid format specification, min cannot be greater than max!", template, fmt);
+            			"Invalid format specification, min cannot be greater than max!", unboundTemplate, fmt);
             }
             if (minFrac > 50 || maxFrac > 50) {// sanity check
-                throw new ParseException("Cannot specify more than 50 fraction digits", template, fmt);
+                throw new ParseException("Cannot specify more than 50 fraction digits", unboundTemplate, fmt);
             }
             result = new NumericalOutput(exp, minFrac, maxFrac);
         } else {  // if format != null
             result = new NumericalOutput(exp);
         }
-        result.setLocation(template, begin, end);
+        result.setLocation(unboundTemplate, begin, end);
         return result;
     }
 }
@@ -1916,7 +1905,7 @@
     block = OptionalBlock()
     {
         cblock = new ConditionalBlock(condition, block, ConditionalBlock.TYPE_IF);
-        cblock.setLocation(template, start, block);
+        cblock.setLocation(unboundTemplate, start, block);
         ifBlock = new IfBlock(cblock);
     }
     (
@@ -1926,7 +1915,7 @@
         block = OptionalBlock()
         {
             cblock = new ConditionalBlock(condition, block, ConditionalBlock.TYPE_ELSE_IF);
-            cblock.setLocation(template, t, block);
+            cblock.setLocation(unboundTemplate, t, block);
             ifBlock.addBlock(cblock);
         }
     )*
@@ -1935,13 +1924,13 @@
             block = OptionalBlock()
             {
                 cblock = new ConditionalBlock(null, block, ConditionalBlock.TYPE_ELSE);
-                cblock.setLocation(template, t, block);
+                cblock.setLocation(unboundTemplate, t, block);
                 ifBlock.addBlock(cblock);
             }
     ]
     end = <END_IF>
     {
-        ifBlock.setLocation(template, start, end);
+        ifBlock.setLocation(unboundTemplate, start, end);
         return ifBlock;
     }
 }
@@ -1963,7 +1952,7 @@
     )
     {
         AttemptBlock result = new AttemptBlock(block, recoveryBlock);
-        result.setLocation(template, start, end);
+        result.setLocation(unboundTemplate, start, end);
         return result;
     }
 }
@@ -1978,7 +1967,7 @@
     block = OptionalBlock()
     {
         RecoveryBlock result = new RecoveryBlock(block);
-        result.setLocation(template, start, block);
+        result.setLocation(unboundTemplate, start, block);
         return result;
     }
 }
@@ -2000,7 +1989,7 @@
     {
         --loopNesting;
         IteratorBlock result = new IteratorBlock(exp, index.image, block, false);
-        result.setLocation(template, start, end);
+        result.setLocation(unboundTemplate, start, end);
         return result;
     }
 }
@@ -2022,7 +2011,7 @@
     {
         --loopNesting;
         IteratorBlock result = new IteratorBlock(exp, index.image, block, true);
-        result.setLocation(template, start, end);
+        result.setLocation(unboundTemplate, start, end);
         return result;
     }
 }
@@ -2042,7 +2031,7 @@
     end = LooseDirectiveEnd()
     {
         VisitNode result = new VisitNode(targetNode, namespaces);
-        result.setLocation(template, start, end);
+        result.setLocation(unboundTemplate, start, end);
         return result;
     }
 }
@@ -2071,7 +2060,7 @@
     {
         if (end == null) end = start;
         RecurseNode result = new RecurseNode(node, namespaces);
-        result.setLocation(template, start, end);
+        result.setLocation(unboundTemplate, start, end);
         return result;
     }
 }
@@ -2084,10 +2073,10 @@
     tok = <FALLBACK>
     {
         if (!inMacro) {
-            throw new ParseException("Cannot fall back outside a macro.", template, tok);
+            throw new ParseException("Cannot fall back outside a macro.", unboundTemplate, tok);
         }
         FallbackInstruction result = new FallbackInstruction();
-        result.setLocation(template, tok, tok);
+        result.setLocation(unboundTemplate, tok, tok);
         return result;
     }
 }
@@ -2104,10 +2093,10 @@
     {
         if (loopNesting < 1 && switchNesting < 1)
         {
-            throw new ParseException(start.image + " occurred outside a loop or a switch block.", template, start);
+            throw new ParseException(start.image + " occurred outside a loop or a switch block.", unboundTemplate, start);
         }
         BreakInstruction result = new BreakInstruction();
-        result.setLocation(template, start, start);
+        result.setLocation(unboundTemplate, start, start);
         return result;
     }
 }
@@ -2130,20 +2119,20 @@
     {
         if (inMacro) {
             if (exp != null) {
-            	throw new ParseException("A macro cannot return a value", template, start);
+            	throw new ParseException("A macro cannot return a value", unboundTemplate, start);
             }
         } else if (inFunction) {
             if (exp == null) {
-            	throw new ParseException("A function must return a value", template, start);
+            	throw new ParseException("A function must return a value", unboundTemplate, start);
             }
         } else {
             if (exp == null) {
             	throw new ParseException(
-            			"A return instruction can only occur inside a macro or function", template, start);
+            			"A return instruction can only occur inside a macro or function", unboundTemplate, start);
             }
         }
         ReturnInstruction result = new ReturnInstruction(exp);
-        result.setLocation(template, start, end);
+        result.setLocation(unboundTemplate, start, end);
         return result;
     }
 }
@@ -2161,7 +2150,7 @@
     )
     {
         StopInstruction result = new StopInstruction(exp);
-        result.setLocation(template, start, start);
+        result.setLocation(unboundTemplate, start, start);
         return result;
     }
 }
@@ -2178,7 +2167,7 @@
             t = <SIMPLE_NESTED>
             {
                 result = new BodyInstruction(null);
-                result.setLocation(template, t, t);
+                result.setLocation(unboundTemplate, t, t);
             }
         )
         |
@@ -2188,13 +2177,13 @@
             end = LooseDirectiveEnd()
             {
                 result = new BodyInstruction(bodyParameters);
-                result.setLocation(template, t, end);
+                result.setLocation(unboundTemplate, t, end);
             }
         )
     )
     {
         if (!inMacro) {
-            throw new ParseException("Cannot use a " + t.image + " instruction outside a macro.", template, t);
+            throw new ParseException("Cannot use a " + t.image + " instruction outside a macro.", unboundTemplate, t);
         }
         return result;
     }
@@ -2208,7 +2197,7 @@
     t = <FLUSH>
     {
         FlushInstruction result = new FlushInstruction();
-        result.setLocation(template, t, t);
+        result.setLocation(unboundTemplate, t, t);
         return result;
     }
 }
@@ -2229,7 +2218,7 @@
         t = <NOTRIM> { result = new TrimInstruction(false, false); }
     )
     {
-        result.setLocation(template, t, t);
+        result.setLocation(unboundTemplate, t, t);
         return result;
     }
 }
@@ -2256,7 +2245,7 @@
         {
             scope = Assignment.LOCAL;
             if (!inMacro && !inFunction) {
-                throw new ParseException("Local variable assigned outside a macro.", template, start);
+                throw new ParseException("Local variable assigned outside a macro.", unboundTemplate, start);
             }
         }
     )
@@ -2272,7 +2261,7 @@
 	        exp = Expression()
 	        {
 	            ass = new Assignment(varName, exp, scope);
-	            ass.setLocation(template, nameExp, exp);
+	            ass.setLocation(unboundTemplate, nameExp, exp);
 	            assignments.add(ass);
 	        }
 	        (
@@ -2288,7 +2277,7 @@
 	            exp = Expression()
 	            {
 	                ass = new Assignment(varName, exp, scope);
-	                ass.setLocation(template, nameExp, exp);
+	                ass.setLocation(unboundTemplate, nameExp, exp);
 	                assignments.add(ass);
 	            } 
 	        )*
@@ -2297,7 +2286,7 @@
 	            nsExp = Expression()
 	            {
 	                if (scope != Assignment.NAMESPACE) {
-	                	throw new ParseException("Cannot assign to namespace here.", template, id);
+	                	throw new ParseException("Cannot assign to namespace here.", unboundTemplate, id);
                 	}
 	            }
 	        ]
@@ -2308,7 +2297,7 @@
 	                ai.addAssignment((Assignment) assignments.get(i));
 	            }
 	            ai.setNamespaceExp(nsExp);
-	            ai.setLocation(template, start, end);
+	            ai.setLocation(unboundTemplate, start, end);
 	            return ai;
 	        }
 	    )
@@ -2319,7 +2308,7 @@
 	            nsExp = Expression()
 	            {
 	                if (scope != Assignment.NAMESPACE) {
-	                	throw new ParseException("Cannot assign to namespace here.", template, id);
+	                	throw new ParseException("Cannot assign to namespace here.", unboundTemplate, id);
 	            	}
 	            }
 	        ]
@@ -2329,26 +2318,26 @@
 	            end = <END_LOCAL>
 	            {
 	            	if (scope != Assignment.LOCAL) {
-	            		throw new ParseException("Mismatched assignment tags.", template, end);
+	            		throw new ParseException("Mismatched assignment tags.", unboundTemplate, end);
 	        		}
 	        	}
 	            |
 	            end = <END_ASSIGN>
 	            {
 	            	if (scope != Assignment.NAMESPACE) {
-	            		throw new ParseException("Mismatched assignment tags.", template, end);
+	            		throw new ParseException("Mismatched assignment tags.", unboundTemplate, end);
 	        		}
 	        	}
 	            |
 	            end = <END_GLOBAL>
 	            {
 	            	if (scope != Assignment.GLOBAL) throw new ParseException(
-	            			"Mismatched assignment tags", template, end);
+	            			"Mismatched assignment tags", unboundTemplate, end);
             	}
 	        )
 	        {
 	            BlockAssignment ba = new BlockAssignment(block, varName, scope, nsExp);
-	            ba.setLocation(template, start, end);
+	            ba.setLocation(unboundTemplate, start, end);
 	            return ba;
 	        }
 	    )
@@ -2387,14 +2376,14 @@
                 		      : " Supporting camelCase parameter names is planned for FreeMarker 2.4.0; "
 	                              + "check if an update is available, and if it indeed supports camel "
 	                              + "case."),
-                		template, att);
+                		unboundTemplate, att);
             }
         }
     )*
     end = LooseDirectiveEnd()
     {
-        Include result = new Include(template, nameExp, encodingExp, parseExp, ignoreMissingExp);
-        result.setLocation(template, start, end);
+        Include result = new Include(unboundTemplate, nameExp, encodingExp, parseExp, ignoreMissingExp);
+        result.setLocation(unboundTemplate, start, end);
         return result;
     }
 }
@@ -2411,9 +2400,9 @@
     ns = <ID>
     end = LooseDirectiveEnd()
     {
-        LibraryLoad result = new LibraryLoad(template, nameExp, ns.image);
-        result.setLocation(template, start, end);
-        template.addImport(result);
+        LibraryLoad result = new LibraryLoad(unboundTemplate, nameExp, ns.image);
+        result.setLocation(unboundTemplate, start, end);
+        unboundTemplate.addImport(result);
         return result;
     }
 }
@@ -2440,7 +2429,7 @@
     )
     {
         if (inMacro || inFunction) {
-            throw new ParseException("Macros cannot be nested.", template, start);
+            throw new ParseException("Macros cannot be nested.", unboundTemplate, start);
         }
         if (isFunction) inFunction = true; else inMacro = true;
     }
@@ -2469,13 +2458,13 @@
             if (catchAll != null) {
                 throw new ParseException(
                 "There may only be one \"catch-all\" parameter in a macro declaration, and it must be the last parameter.",
-                template, arg);
+                unboundTemplate, arg);
             }
             if (isCatchAll) {
                 if (defValue != null) {
                     throw new ParseException(
                     "\"Catch-all\" macro parameter may not have a default value.",
-                    template, arg);
+                    unboundTemplate, arg);
                 }
                 catchAll = arg.image;
             } else {
@@ -2484,7 +2473,7 @@
                     throw new ParseException(
 		                    "In a macro declaration, parameters without a default value "
 		                    + "must all occur before the parameters with default values.",
-                    template, arg);
+                    unboundTemplate, arg);
                 }
                 args.put(arg.image, defValue);
             }
@@ -2496,19 +2485,19 @@
     (
         end = <END_MACRO>
         {
-        	if(isFunction) throw new ParseException("Expected function end tag here.", template, start);
+        	if(isFunction) throw new ParseException("Expected function end tag here.", unboundTemplate, start);
     	}
         |
         end = <END_FUNCTION>
         {
-    		if(!isFunction) throw new ParseException("Expected macro end tag here.", template, start);
+    		if(!isFunction) throw new ParseException("Expected macro end tag here.", unboundTemplate, start);
     	}
     )
     {
         inMacro = inFunction = false;
-        Macro result = new Macro(name, argNames, args, catchAll, isFunction, block);
-        result.setLocation(template, start, end);
-        template.addMacro(result);
+        UnboundCallable result = new UnboundCallable(name, argNames, args, catchAll, isFunction, block);
+        result.setLocation(unboundTemplate, start, end);
+        unboundTemplate.addUnboundCallable(result);
         return result;
     }
 }
@@ -2524,7 +2513,7 @@
     end = <END_COMPRESS>
     {
         CompressedBlock cb = new CompressedBlock(block);
-        cb.setLocation(template, start, end);
+        cb.setLocation(unboundTemplate, start, end);
         return cb;
     }
 }
@@ -2575,9 +2564,9 @@
                 s = s.substring(0, s.length() -1).trim();
                 if (s.length() >0 && !s.equals(directiveName)) {
                     if (directiveName == null) {
-                        throw new ParseException("Expecting </@>", template, end);
+                        throw new ParseException("Expecting </@>", unboundTemplate, end);
                     } else {
-                        throw new ParseException("Expecting </@> or </@" + directiveName + ">", template, end);
+                        throw new ParseException("Expecting </@> or </@" + directiveName + ">", unboundTemplate, end);
                     }
                 }
             }
@@ -2587,7 +2576,7 @@
         TemplateElement result = (positionalArgs != null)
         		? new UnifiedCall(exp, positionalArgs, nestedBlock, bodyParameters)
 	            : new UnifiedCall(exp, namedArgs, nestedBlock, bodyParameters);
-        result.setLocation(template, start, end);
+        result.setLocation(unboundTemplate, start, end);
         return result;
     }
 }
@@ -2624,7 +2613,7 @@
             result = new UnifiedCall(new Identifier(macroName), namedArgs, null, null);
         }
         result.legacySyntax = true;
-        result.setLocation(template, start, end);
+        result.setLocation(unboundTemplate, start, end);
         return result;
     }
 }
@@ -2687,7 +2676,7 @@
     end = UnparsedContent(start, buf)
     {
         Comment result = new Comment(buf.toString());
-        result.setLocation(template, start, end);
+        result.setLocation(unboundTemplate, start, end);
         return result;
     }
 }
@@ -2702,7 +2691,7 @@
     end = UnparsedContent(start, buf)
     {
         TextBlock result = new TextBlock(buf.toString(), true);
-        result.setLocation(template, start, end);
+        result.setLocation(unboundTemplate, start, end);
         return result;
     }
 }
@@ -2738,7 +2727,7 @@
     )
     {
         TransformBlock result = new TransformBlock(exp, args, content);
-        result.setLocation(template, start, end);
+        result.setLocation(unboundTemplate, start, end);
         return result;
     }
 }
@@ -2766,7 +2755,7 @@
             if (caseIns.condition == null) {
                 if (defaultFound) {
                     throw new ParseException(
-                    "You can only have one default case in a switch statement", template, start);
+                    "You can only have one default case in a switch statement", unboundTemplate, start);
                 }
                 defaultFound = true;
             }
@@ -2777,7 +2766,7 @@
     end = <END_SWITCH>
     {
         --switchNesting;
-        switchBlock.setLocation(template, start, end);
+        switchBlock.setLocation(unboundTemplate, start, end);
         return switchBlock;
     }
 }
@@ -2798,7 +2787,7 @@
     block = OptionalBlock()
     {
         Case result = new Case(exp, block);
-        result.setLocation(template, start, block);
+        result.setLocation(unboundTemplate, start, block);
         return result;
     }
 }
@@ -2826,7 +2815,7 @@
     }
     end = <END_ESCAPE>
     {
-        result.setLocation(template, start, end);
+        result.setLocation(unboundTemplate, start, end);
         return result;
     }
 }
@@ -2840,7 +2829,7 @@
     start = <NOESCAPE>
     {
         if (escapes.isEmpty()) {
-            throw new ParseException("noescape with no matching escape encountered.", template, start);
+            throw new ParseException("noescape with no matching escape encountered.", unboundTemplate, start);
         }
         Object escape = escapes.removeFirst();
     }
@@ -2849,7 +2838,7 @@
     {
         escapes.addFirst(escape);
         NoEscapeBlock result = new NoEscapeBlock(content);
-        result.setLocation(template, start, end);
+        result.setLocation(unboundTemplate, start, end);
         return result;
     }
 }
@@ -2885,7 +2874,7 @@
     end = LooseDirectiveEnd()
     {
         PropertySetting result = new PropertySetting(key.image, value);
-        result.setLocation(template, start, end);
+        result.setLocation(unboundTemplate, start, end);
         return result;
     }
 }
@@ -2989,7 +2978,7 @@
         if (stripText && mixedContentNesting == 1) return TextBlock.EMPTY_BLOCK;
 
         TextBlock result = new TextBlock(buf.toString(), false);
-        result.setLocation(template, start, t);
+        result.setLocation(unboundTemplate, start, t);
         return result;
     }
 }
@@ -3014,8 +3003,8 @@
     {
         buf.setLength(buf.length() - t.image.length());
         if (!t.image.endsWith(";")
-                && _TemplateAPI.getTemplateLanguageVersionAsInt(template) >= _TemplateAPI.VERSION_INT_2_3_21) {
-            throw new ParseException("Unclosed \"" + start.image + "\"", template, start);
+                && unboundTemplate.getTemplateLanguageVersion().intValue() >= _TemplateAPI.VERSION_INT_2_3_21) {
+            throw new ParseException("Unclosed \"" + start.image + "\"", unboundTemplate, start);
         }
         return t;
     }
@@ -3048,7 +3037,7 @@
     )+
     {
         mixedContentNesting--;
-        mixedContent.setLocation(template, begin, elem);
+        mixedContent.setLocation(unboundTemplate, begin, elem);
         return mixedContent;
     }
 }
@@ -3079,7 +3068,7 @@
         }
     )+
     {
-        nodes.setLocation(template, begin, elem);
+        nodes.setLocation(unboundTemplate, begin, elem);
         return nodes;
     }
 }
@@ -3136,14 +3125,13 @@
                             vs = ((TemplateScalarModel) exp).getAsString();
                         } catch (TemplateModelException tme) {}
                     }
-                    if (template != null) {
+                    if (unboundTemplate != null) {
                         if (ks.equalsIgnoreCase("encoding")) {
                             if (vs == null) {
                                 throw new ParseException("Expecting an encoding string.", exp);
                             }
-                            String encoding = template.getEncoding();
-                            if (encoding != null && !encoding.equalsIgnoreCase(vs)) {
-                                throw new Template.WrongEncodingException(vs, encoding);
+                            if (assumedEncoding != null && !assumedEncoding.equalsIgnoreCase(vs)) {
+                                throw new Template.WrongEncodingException(vs, assumedEncoding);
                             }
                         } else if (ks.equalsIgnoreCase("STRIP_WHITESPACE")) {
                             this.stripWhitespace = getBoolean(exp);
@@ -3166,7 +3154,7 @@
                                     }
                                     String nsURI = ((TemplateScalarModel) valueModel).getAsString();
                                     try {
-                                        template.addPrefixNSMapping(prefix, nsURI);
+                                        unboundTemplate.addPrefixToNamespaceURIMapping(prefix, nsURI);
                                     } catch (IllegalArgumentException iae) {
                                         throw new ParseException(iae.getMessage(), exp);
                                     }
@@ -3183,7 +3171,7 @@
                                 for (TemplateModelIterator it = keys.iterator(); it.hasNext();) {
                                         String attName = ((TemplateScalarModel) it.next()).getAsString();
                                         Object attValue = DeepUnwrap.unwrap(attributeMap.get(attName));
-                                        template.setCustomAttribute(attName, attValue);
+                                        unboundTemplate.setCustomAttribute(attName, attValue);
                                 }
                             } catch (TemplateModelException tme) {
                             }
@@ -3207,7 +3195,7 @@
                                             : ". Supporting camelCase parameter names is planned for FreeMarker 2.4.0; "
                                               + "check if an update is available, and if it indeed supports camel "
                                               + "case. Until that, use " + correctName + " instead."),
-                                    template, key);
+                                    unboundTemplate, key);
                         }
                     }
                 }
diff --git a/src/test/java/freemarker/core/TestEnvironmentGetTemplateVariants.java b/src/test/java/freemarker/core/TestEnvironmentGetTemplateVariants.java
index 6bb00b9..6e4f946 100644
--- a/src/test/java/freemarker/core/TestEnvironmentGetTemplateVariants.java
+++ b/src/test/java/freemarker/core/TestEnvironmentGetTemplateVariants.java
@@ -120,6 +120,19 @@
         TEMPLATES.putTemplate("inc4",
                 "<@tNames />"
                 );
+        
+        TEMPLATES.putTemplate("FM23MacroNsBug/main",
+                "<#include 'inc'>"
+                + "<#assign ns = 'main'>"
+                + "<@m />\n"
+                + "<#import 'inc' as i>"
+                + "<@i.m />\n"
+                + "<@m />\n");
+        TEMPLATES.putTemplate("FM23MacroNsBug/inc",
+                "<#assign ns = 'inc'>"
+                + "<#macro m>"
+                    + "ns: ${ns}"
+                + "</#macro>");
     }
     
     private final static String EXPECTED_2_3_21 =
@@ -129,31 +142,31 @@
             + "---2---\n"
             + "[impM: <t=main ct=imp mt=main>\n"
                 + "{<t=main ct=main mt=main>}\n"
-                + "[inc: <t=inc ct=inc mt=main>\n"
-                    + "[incM: <t=inc ct=inc mt=main> {<t=imp ct=inc mt=main>}]\n"
-                    + "[incInc: <t=inc ct=inc mt=main>\n"
-                        + "[incM: <t=inc ct=inc mt=main> {<t=imp ct=inc mt=main>}]\n"
+                + "[inc: <t=inc ct=inc cnst=imp mt=main>\n"
+                    + "[incM: <t=inc ct=inc cnst=imp mt=main> {<t=imp ct=inc cnst=imp mt=main>}]\n"
+                    + "[incInc: <t=inc ct=inc cnst=imp mt=main>\n"
+                        + "[incM: <t=inc ct=inc cnst=imp mt=main> {<t=imp ct=inc cnst=imp mt=main>}]\n"
                     + "]\n"
                 + "]\n"
-                + "[incM: <t=main ct=inc mt=main> {<t=imp ct=imp mt=main>}]\n"
+                + "[incM: <t=main ct=inc cnst=imp mt=main> {<t=imp ct=imp mt=main>}]\n"
             + "]\n"
             + "---3---\n"
-            + "[inc: <t=inc ct=inc mt=main>\n"
-                + "[incM: <t=inc ct=inc mt=main> {<t=main ct=inc mt=main>}]\n"
-                + "[incInc: <t=inc ct=inc mt=main>\n"
-                    + "[incM: <t=inc ct=inc mt=main> {<t=main ct=inc mt=main>}]\n"
+            + "[inc: <t=inc ct=inc cnst=main mt=main>\n"
+                + "[incM: <t=inc ct=inc cnst=main mt=main> {<t=main ct=inc cnst=main mt=main>}]\n"
+                + "[incInc: <t=inc ct=inc cnst=main mt=main>\n"
+                    + "[incM: <t=inc ct=inc cnst=main mt=main> {<t=main ct=inc cnst=main mt=main>}]\n"
                 + "]\n"
             + "]\n"
             + "---4---\n"
-            + "[incM: <t=main ct=inc mt=main> {<t=main ct=main mt=main>}]\n"
+            + "[incM: <t=main ct=inc cnst=main mt=main> {<t=main ct=main mt=main>}]\n"
             + "---5---\n"
-            + "[inc2: <t=inc2 ct=inc2 mt=main>\n"
+            + "[inc2: <t=inc2 ct=inc2 cnst=main mt=main>\n"
                 + "[impM: <t=inc2 ct=imp mt=main>\n"
-                    + "{<t=main ct=inc2 mt=main>}\n"
-                    + "[inc: <t=inc ct=inc mt=main>\n"
-                        + "[incM: <t=inc ct=inc mt=main> {<t=imp ct=inc mt=main>}]\n"
+                    + "{<t=main ct=inc2 cnst=main mt=main>}\n"
+                    + "[inc: <t=inc ct=inc cnst=imp mt=main>\n"
+                        + "[incM: <t=inc ct=inc cnst=imp mt=main> {<t=imp ct=inc cnst=imp mt=main>}]\n"
                     + "]\n"
-                    + "[incM: <t=inc2 ct=inc mt=main> {<t=imp ct=imp mt=main>}]\n"
+                    + "[incM: <t=inc2 ct=inc cnst=imp mt=main> {<t=imp ct=imp mt=main>}]\n"
                 + "]\n"
             + "]\n"
             + "---6---\n"
@@ -162,19 +175,19 @@
                 + "[imp2M: <t=main ct=imp2 mt=main> {<t=imp ct=imp mt=main>}]\n"
             + "]\n"
             + "---7---\n"
-            + "[inc3: <t=inc3 ct=inc3 mt=main>\n"
-                + "[mainM: <t=inc3 ct=main mt=main> {<t=main ct=inc3 mt=main>} <t=inc3 ct=main mt=main>]\n"
+            + "[inc3: <t=inc3 ct=inc3 cnst=main mt=main>\n"
+                + "[mainM: <t=inc3 ct=main mt=main> {<t=main ct=inc3 cnst=main mt=main>} <t=inc3 ct=main mt=main>]\n"
             + "]\n"
             + "[mainM: "
                 + "<t=main ct=main mt=main> "
-                + "{<t=main ct=main mt=main> <t=inc4 ct=inc4 mt=main> <t=main ct=main mt=main>} "
+                + "{<t=main ct=main mt=main> <t=inc4 ct=inc4 cnst=main mt=main> <t=main ct=main mt=main>} "
                 + "<t=main ct=main mt=main>"
             + "]\n"
             + "<t=main ct=main mt=main>\n"
             + "---8---\n"
-            + "mainF: <t=main ct=main mt=main>, impF: <t=main ct=imp mt=main>, incF: <t=main ct=inc mt=main>\n"
-            ;
-
+            + "mainF: <t=main ct=main mt=main>, impF: <t=main ct=imp mt=main>, "
+            + "incF: <t=main ct=inc cnst=main mt=main>\n";
+    
     @Test
     public void test2321() throws IOException, TemplateException {
         setConfiguration(createConfiguration(Configuration.VERSION_2_3_21));
@@ -194,6 +207,15 @@
         assertSame(t, env.getMainTemplate());
         assertSame(t, env.getCurrentTemplate());
     }
+
+    @Test
+    public void testFM23MacroNsBugFixed() throws IOException, TemplateException {
+        setConfiguration(createConfiguration(Configuration.VERSION_2_3_0));
+        assertOutputForNamed("FM23MacroNsBug/main",
+                "ns: main\n"
+                + "ns: inc\n"
+                + "ns: main\n");
+    }
     
     private Configuration createConfiguration(Version version2321) {
         Configuration cfg = new Configuration(version2321);
@@ -209,8 +231,11 @@
             public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
                     throws TemplateException, IOException {
                 Writer out = env.getOut();
-                final String r = "<t=" + env.getTemplate().getName() + " ct=" + env.getCurrentTemplate().getName() + " mt="
-                        + env.getMainTemplate().getName() + ">";
+                final Template currentTemplate = env.getCurrentTemplate();
+                final Template curNsTemplate = env.getCurrentNamespace().getTemplate();
+                final String r = "<t=" + env.getTemplate().getName() + " ct=" + currentTemplate.getName()
+                        + (curNsTemplate == currentTemplate ? "" : " cnst=" + curNsTemplate.getName())
+                        + " mt=" + env.getMainTemplate().getName() + ">";
                 out.write(r);
                 env.setGlobalVariable("lastTNamesResult", new SimpleScalar(r));
             }
diff --git a/src/test/java/freemarker/template/CustomAttributeTest.java b/src/test/java/freemarker/template/CustomAttributeTest.java
index 1394f1b..4ec2de0 100644
--- a/src/test/java/freemarker/template/CustomAttributeTest.java
+++ b/src/test/java/freemarker/template/CustomAttributeTest.java
@@ -19,7 +19,6 @@
 import static org.junit.Assert.*;
 
 import java.math.BigDecimal;
-import java.util.Arrays;
 
 import org.junit.Test;
 
@@ -62,17 +61,17 @@
         assertSame(VALUE_1, t.getCustomAttribute(KEY_1));
         
         t.setCustomAttribute(KEY_2, VALUE_2);
-        assertArrayEquals(new String[] { KEY_1, KEY_2 }, sort(t.getCustomAttributeNames()));        
+        assertArrayEquals(new String[] { KEY_1, KEY_2 }, t.getCustomAttributeNames());        
         assertSame(VALUE_1, t.getCustomAttribute(KEY_1));
         assertSame(VALUE_2, t.getCustomAttribute(KEY_2));
         
         t.setCustomAttribute(KEY_1, VALUE_2);
-        assertArrayEquals(new String[] { KEY_1, KEY_2 }, sort(t.getCustomAttributeNames()));        
+        assertArrayEquals(new String[] { KEY_1, KEY_2 }, t.getCustomAttributeNames());        
         assertSame(VALUE_2, t.getCustomAttribute(KEY_1));
         assertSame(VALUE_2, t.getCustomAttribute(KEY_2));
         
         t.setCustomAttribute(KEY_1, null);
-        assertArrayEquals(new String[] { KEY_1, KEY_2 }, sort(t.getCustomAttributeNames()));        
+        assertArrayEquals(new String[] { KEY_1, KEY_2 }, t.getCustomAttributeNames());        
         assertNull(t.getCustomAttribute(KEY_1));
         assertSame(VALUE_2, t.getCustomAttribute(KEY_2));
         
@@ -102,7 +101,7 @@
                 + "}>",
                 new Configuration(Configuration.VERSION_2_3_22));
         
-        assertArrayEquals(new String[] { KEY_1, KEY_2 }, sort(t.getCustomAttributeNames()));
+        assertArrayEquals(new String[] { KEY_1, KEY_2 }, t.getCustomAttributeNames());
         assertEquals(VALUE_LIST, t.getCustomAttribute(KEY_1));
         assertEquals(VALUE_BIGDECIMAL, t.getCustomAttribute(KEY_2));
         
@@ -112,7 +111,7 @@
     }
     
     @Test
-    public void testFtl2Header() throws Exception {
+    public void testFtlHeader2() throws Exception {
         Template t = new Template(null, "<#ftl attributes={"
                 + "'" + KEY_1 + "': 'a', "
                 + "'" + KEY_2 + "': 'b', "
@@ -120,20 +119,20 @@
                 + "}>",
                 new Configuration(Configuration.VERSION_2_3_22));
         
-        assertArrayEquals(new String[] { KEY_1, KEY_2, KEY_3 }, sort(t.getCustomAttributeNames()));
+        assertArrayEquals(new String[] { KEY_1, KEY_2, KEY_3 }, t.getCustomAttributeNames());
         assertEquals("a", t.getCustomAttribute(KEY_1));
         assertEquals("b", t.getCustomAttribute(KEY_2));
         assertEquals("c", t.getCustomAttribute(KEY_3));
         
         t.removeCustomAttribute(KEY_2);
-        assertArrayEquals(new String[] { KEY_1, KEY_3 }, sort(t.getCustomAttributeNames()));
+        assertArrayEquals(new String[] { KEY_1, KEY_3 }, t.getCustomAttributeNames());
         assertEquals("a", t.getCustomAttribute(KEY_1));
         assertNull(t.getCustomAttribute(KEY_2));
         assertEquals("c", t.getCustomAttribute(KEY_3));
     }
 
     @Test
-    public void testFtl3Header() throws Exception {
+    public void testFtlHeader3() throws Exception {
         Template t = new Template(null, "<#ftl attributes={"
                 + "'" + KEY_1 + "': 'a', "
                 + "'" + KEY_2 + "': 'b', "
@@ -141,21 +140,28 @@
                 + "}>",
                 new Configuration(Configuration.VERSION_2_3_22));
         
-        assertArrayEquals(new String[] { KEY_1, KEY_2, KEY_3 }, sort(t.getCustomAttributeNames()));
+        assertArrayEquals(new String[] { KEY_1, KEY_2, KEY_3 }, t.getCustomAttributeNames());
         assertEquals("a", t.getCustomAttribute(KEY_1));
         assertEquals("b", t.getCustomAttribute(KEY_2));
         assertEquals("c", t.getCustomAttribute(KEY_3));
         
         t.setCustomAttribute(KEY_2, null);
-        assertArrayEquals(new String[] { KEY_1, KEY_2, KEY_3 }, sort(t.getCustomAttributeNames()));
+        assertArrayEquals(new String[] { KEY_1, KEY_2, KEY_3 }, t.getCustomAttributeNames());
         assertEquals("a", t.getCustomAttribute(KEY_1));
         assertNull(t.getCustomAttribute(KEY_2));
         assertEquals("c", t.getCustomAttribute(KEY_3));
     }
     
-    private Object[] sort(String[] customAttributeNames) {
-        Arrays.sort(customAttributeNames);
-        return customAttributeNames;
+    @Test
+    public void testFtlHeaderAndNullValue() throws Exception {
+        final Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
+        
+        final Template t = new Template(null, "[#ftl attributes={ '" + KEY_1 + "': 'u' }]", cfg);
+        assertEquals("u", t.getCustomAttribute(KEY_1));
+        t.setCustomAttribute(KEY_1, null);
+        assertNull(t.getCustomAttribute(KEY_1));
+        t.removeCustomAttribute(KEY_1);
+        assertEquals(null, t.getCustomAttribute(KEY_1));
     }
 
     @Test
@@ -202,6 +208,58 @@
         env.process();
     }
 
+    @Test
+    public void testScopesFallback() throws Exception {
+        final Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
+        
+        final Template t = new Template(null, "[#ftl attributes={ '" + KEY_1 + "': 't' }]", cfg);
+        Environment env = t.createProcessingEnvironment(this, NullWriter.INSTANCE);
+        
+        assertEquals("t", env.getCustomAttribute(KEY_1));
+        assertEquals("t", t.getCustomAttribute(KEY_1));
+        assertNull(cfg.getCustomAttribute(KEY_1));
+        
+        env.setCustomAttribute(KEY_1, "e");
+        assertEquals("e", env.getCustomAttribute(KEY_1));
+        assertEquals("t", t.getCustomAttribute(KEY_1));
+        assertNull(cfg.getCustomAttribute(KEY_1));
+        
+        env.setCustomAttribute(KEY_1, null);
+        assertNull(env.getCustomAttribute(KEY_1));
+        assertEquals("t", t.getCustomAttribute(KEY_1));
+        assertNull(cfg.getCustomAttribute(KEY_1));
+        
+        env.removeCustomAttribute(KEY_1);
+        assertEquals("t", env.getCustomAttribute(KEY_1));
+        assertEquals("t", t.getCustomAttribute(KEY_1));
+        assertNull(cfg.getCustomAttribute(KEY_1));
+        
+        cfg.setCustomAttribute(KEY_2, "c2");
+        assertEquals("c2", env.getCustomAttribute(KEY_2));
+        assertEquals("c2", t.getCustomAttribute(KEY_2));
+        assertEquals("c2", cfg.getCustomAttribute(KEY_2));
+        
+        t.setCustomAttribute(KEY_2, "t2");
+        assertEquals("t2", env.getCustomAttribute(KEY_2));
+        assertEquals("t2", t.getCustomAttribute(KEY_2));
+        assertEquals("c2", cfg.getCustomAttribute(KEY_2));
+        
+        t.setCustomAttribute(KEY_2, null);
+        assertNull(env.getCustomAttribute(KEY_2));
+        assertNull(t.getCustomAttribute(KEY_2));
+        assertEquals("c2", cfg.getCustomAttribute(KEY_2));
+        
+        t.removeCustomAttribute(KEY_2);
+        assertEquals("c2", env.getCustomAttribute(KEY_2));
+        assertEquals("c2", t.getCustomAttribute(KEY_2));
+        assertEquals("c2", cfg.getCustomAttribute(KEY_2));
+        
+        cfg.setCustomAttribute(KEY_2, "c2+");
+        assertEquals("c2+", env.getCustomAttribute(KEY_2));
+        assertEquals("c2+", t.getCustomAttribute(KEY_2));
+        assertEquals("c2+", cfg.getCustomAttribute(KEY_2));
+    }
+    
     public void testScopesFromTemplateStep1() throws Exception {
         assertNull(CUST_ATT_TMP_1.get());
         assertEquals(123, CUST_ATT_TMP_2.get());
@@ -212,13 +270,5 @@
         assertNull(CUST_ATT_CFG_1.get());
         assertEquals(12345, CUST_ATT_CFG_2.get());
     }
-
-    public void testScopesFromTemplateStep2() throws Exception {
-        
-    }
-
-    public void testScopesFromTemplateStep3() throws Exception {
-        
-    }
     
 }
diff --git a/src/test/java/freemarker/template/MistakenlyPublicImportAPIsTest.java b/src/test/java/freemarker/template/MistakenlyPublicImportAPIsTest.java
index 6eb8236..6861e3b 100644
--- a/src/test/java/freemarker/template/MistakenlyPublicImportAPIsTest.java
+++ b/src/test/java/freemarker/template/MistakenlyPublicImportAPIsTest.java
@@ -59,7 +59,7 @@
                 t2.process(null, NullWriter.INSTANCE);
                 fail();
             } catch (InvalidReferenceException e) {
-                // Apparenly, it has never worked like this...
+                // Apparently, it has never worked like this...
                 assertEquals("i1", e.getBlamedExpressionString());
             }
         }
@@ -89,14 +89,10 @@
             StringWriter sw = new StringWriter();
             env = t2.createProcessingEnvironment(null, sw);
             env.setVariable("i2", i2);
-            
-            try {
-                env.process();
-                assertEquals("2", sw.toString());
-            } catch (NullPointerException e) {
-                // Expected on 2.3.x, because it won't find the namespace for the macro
-                // [2.4] Fix this "bug"
-            }
+
+            // Works since 2.4.0, was NPE earlier
+            env.process();
+            assertEquals("2", sw.toString());
         }
     }
     
diff --git a/src/test/resources/freemarker/core/ast-1.ast b/src/test/resources/freemarker/core/ast-1.ast
index 617f20a..c656bd7 100644
--- a/src/test/resources/freemarker/core/ast-1.ast
+++ b/src/test/resources/freemarker/core/ast-1.ast
@@ -95,7 +95,7 @@
                 - content: "more"  // String
     #text  // f.c.TextBlock
         - content: "\n6 "  // String
-    #macro  // f.c.Macro
+    #macro  // f.c.UnboundCallable
         - assignment target: "foo"  // String
         - parameter name: "x"  // String
         - parameter default: null  // Null
@@ -112,7 +112,7 @@
             - passed value: y  // f.c.Identifier
     #text  // f.c.TextBlock
         - content: "\n7 "  // String
-    #function  // f.c.Macro
+    #function  // f.c.UnboundCallable
         - assignment target: "foo"  // String
         - parameter name: "x"  // String
         - parameter default: null  // Null
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl b/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl
index 1358ff3..39a20f4 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl
@@ -17,4 +17,5 @@
 <#assign foo = "x">
 ${.vars['foo']} == x
 <#assign works = .version>
-${.now?is_datetime?c} == true
\ No newline at end of file
+${.now?is_datetime?c} == true
+<@.pass />
\ No newline at end of file